| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| Example | Example |
| Reference | Reference |
Use this page to build screen-space UI β HUDs, menus, and on-screen controls β from Ui* scene nodes and script.
- HUDs β score, health bars, ammo, minimaps: a
UiPanelwithUiLabel/UiImagechildren, updated from scripts viawith_node_mut!(ctx.run, UiLabel, id, |l| l.set_text(...)). - Menus β title, pause, and settings screens:
UiButton/UiImageButtonlaid out in aUiVLayoutorUiGrid, reacting to their click signals. - Inventory and shop screens: a
UiGridinside aUiScrollContainer, populated at runtime (often with a node collection). - Editor-style panels:
UiTreeList,UiDropdown,UiColorPicker,UiTextBox, andUiCheckbox. - Dialogs and toasts layered over gameplay: a
UiPanelwith aUiStyle, anchored over the scene.
Use Ui* nodes for persistent, interactive screen structure. Use Draw2D for transient shapes redrawn each frame that need no focus, layout, or input state. Let gameplay own the source value and send the UI a method call or signal; do not make a label query the world each frame when the owner already knows the value changed.
Scene: a top-left HUD panel with a score label.
[hud]
[UiPanel]
anchor = "tl"
size_ratio = (0.25, 0.12)
translation_ratio = (0.15, -0.15)
style = { fill = "#111827CC" stroke = "#93A4B8" radius = 0.15 }
[/UiPanel]
[/hud]
[score]
parent = @hud
[UiLabel]
text = "Score: 0"
text_size_ratio = 0.5
[/UiLabel]
[/score]
Script: update the label text.
methods!({
fn set_score(&self, ctx: &mut ScriptContext<'_, API>, score: NodeID, value: i64) {
let _ = with_node_mut!(ctx.run, UiLabel, score, |label| {
label.set_text(format!("Score: {value}"));
});
}
});UI nodes are data-only scene nodes backed by perro_ui.
They use UiNode as their base node type.
UiNode
- UiPanel
- UiShape
- UiImage
- UiNineSlice
- UiAnimatedImage
- UiCameraStream
- UiSubView
- UiVideoPlayer
- UiLabel
- UiButton
- UiImageButton
- UiCheckbox
- UiDropdown
- UiColorPicker
- UiTextBox
- UiTextBlock
- UiScrollContainer
- UiLayout
- UiHLayout
- UiVLayout
- UiGrid
- UiTreeList
Every Ui* type above loads from a .scn scene and mutates from script.
All inherit UiNode layout fields.
UiNode
- Invisible UI container.
- Holds
layout,visible,input_enabled, andmouse_filter. - Holds
modulate,self_modulate, andchildren_modulateRGBA multipliers likeNode2D. - Can have UI children.
- Use it to group children, move them together, or create a padded child area.
UiPanel
- Drawn rect.
- Holds
style. - Can have children.
UiShape
- Drawn primitive shape.
- Holds
kind(rect|circle|triangle),fill,stroke,stroke_width. - Default
kindisrect;fillwhite;stroketransparent;stroke_width0. - Use it for solid dots, arrows, markers, and simple shapes without a style block.
UiButton
- Clickable panel.
- Holds normal, hover, and pressed styles.
hoverandpressedcan override layout / transform / style fields.- Add text, image, layouts, grids, or panels as children.
- Emits default
<node_name>_<event>signals plus extra event signal lists.
UiImage
UiSubView
- Owns an isolated mixed 2D/3D child render scope inside its computed UI rect.
- Renders
Node3Ddescendants with its implicit 3D view; noCamera3Dchild is required. - Defaults to a view at
(0, 0, 5)looking toward local(0, 0, 0). - Composites
Node2Ddescendants over the local 3D pass with its implicit 2D view. - Uses an active descendant
Camera3DorCamera2Dinstead of the matching implicit view. - Keeps the other implicit view when only one camera type is active.
- Chooses active local cameras with the same winner rules as the main world.
- Keeps spatial descendants out of the main world render pass.
- Clips the final image to the viewport's UI clip rect and optional corner radius.
- Uses the computed UI rect at 1x, rounded to a 64px long-axis bucket, unless
resolutionoverrides it. - Keeps transparent backgrounds premultiplied for clean filtered edges.
- Tone-maps local 3D HDR once before linear UI composition.
- Treats
backgroundalpha below1as transparent and skips the local sky fill. - Suspends descendant script/internal updates and disables local physics bodies/joints while hidden by default.
- Uses
resolution,aspect_mode,view_position,view_rotation,background, and camera projection fields.
[preview]
[UiSubView]
size = (320, 180)
resolution = (320, 180)
view_position = (0, 0, 5)
background = "#00000000"
corner_radius = 0.08
[/UiSubView]
[/preview]
[model]
parent = @preview
[MeshInstance3D]
mesh = "res://models/item.pmesh"
[/MeshInstance3D]
[/model]
[light]
parent = @preview
[RayLight3D]
rotation = (-0.35, 0.25, 0, 0.9)
[/RayLight3D]
[/light]
Use UiCameraStream for a view into the existing world.
Use UiSubView for a UI-owned isolated child scene.
Legacy [UiViewport] scene blocks load as UiSubView.
UiCameraStream and UiSubView solve different ownership problems:
-
UiCameraStreamdisplays an explicitly referenced camera that physically belongs to the existing world. Use it for split-screen views of one shared world, minimaps, security cameras, rear-view mirrors, and other in-game camera feeds. -
UiSubViewowns a separate local render scope. Its descendants do not appear in the main world; they only appear through the owning SubView. -
SubView ownership behaves like an automatic renderer mask around the descendant scope. It does not create a separate script or physics runtime.
-
Drawn image node.
-
Holds
texture,texture_region,tint,scale_mode, alignment, andaspect_ratio. -
Use it for icon, portrait, inventory, and image-heavy UI.
UiImageButton
- Clickable image node.
- Holds image fields plus button input, hover, pressed, and extra click signal fields.
hoverandpressedcan override layout / transform fields andtint.- Emits default
<node_name>_<event>signals plus extra event signal lists. - Use it for icon buttons, irregular-looking buttons, HUD slots, and image-only controls.
UiCheckbox
UiButtonthat toggles acheckedbool.- Adds
checked,dot_fill, andchecked_style/checked_hover_style/checked_pressed_styleon top of button fields. - Emits the same button signals; read
checkedto branch. - Use it for options, toggles, and settings rows.
UiDropdown
UiButtonthat opens an option-list popup.- Holds
options(label/value),selected_index,open,popup_style,option_style,option_height. popup_size = (width, height)resizes list.0axis use button width or option-list height.popup_offsetmoves list.popup_direction=down,up,left, orright.open_animation=poporextend.open_animation_durationuses sec.- Emits
selected_signalson choice. - Use it for enum pickers, quality settings, and menu selects.
UiColorPicker
UiButtonplus a popup that edits acolor.- Holds
color,popup_open,popup_style,popup_size, andwheel_radius. picker_modeselectssmooth_wheel,block_wheel, orswatches.show_selector,show_hex,show_rgba, andshow_hslcontrol popup sections.- Popup height grows when needed, so large selectors never overlap value fields.
- Defaults:
smooth_wheel; all four popup sections visible;popup_size = (360, 344);wheel_radius = 88.0. smooth_wheelgives continuous hue/saturation.block_wheelsnaps to 12 hue bands and 4 saturation bands.swatchesgives a fixed 6-by-4 palette.- Selector picks preserve alpha. Wheel picks use full HSV value; RGBA, HSL, or hex fields can refine the result.
- RGBA fields use
0.0..1.0. HSL uses hue degrees plus0.0..1.0saturation/lightness and preserves alpha. - Hex shows
#RRGGBBAA; edits accept#RGB,#RGBA,#RRGGBB, or#RRGGBBAA. Forms without alpha preserve current alpha. - Emits
color_changed_signals. - Use it for editor swatches, theme pickers, and tint controls.
UiNineSlice
- Scalable image panel.
- Holds
texture,texture_region,margins, andtint. - Keeps corners fixed and tiles edges and center.
- Use it as a child/background for UI containers and buttons.
UiAnimatedImage
- Animated image node for UI space.
- Holds
texture, named sprite-sheetanimations, playback fields,tint,scale_mode, alignment, andaspect_ratio. - Uses same strip/grid animation data shape as
AnimatedSprite2D. - Use it for animated icons, portraits, cooldowns, indicators, and HUD effects.
UiCameraStream
- Draws a camera's render target into UI space.
- Streams an explicitly referenced camera from the existing unified world.
- Uses transparent pixels where the source camera renders no object.
- Omits the visual
Sky3Dbackground while retaining sky lighting and environment effects. - Tone-maps camera-rendered 2D/3D HDR once before UI composition.
- Keeps transparent stream pixels premultiplied for filtered alpha edges.
- Leaves display-referred webcam frames out of the scene tone-map pass.
- Holds
camera,resolution,aspect_ratio,aspect_mode,enabled,tint,post_processing, andcorner_radius. - Use it for split screen, minimaps, security-feed panels, and picture-in-picture views.
UiVideoPlayer
- Plays video into UI space.
- Holds video source / playback fields plus
tint,scale_mode,aspect_ratio, andcorner_radius. - Default
scale_modeisfit;aspect_ratio1.0. - Use it for cutscene panels, menu backdrops, and in-UI clips.
UiLabel
- Text visual.
- Holds
text,color,text_size_ratio, and text alignment. - Can have children, but usually should not.
- Use
Label2DorLabel3Dfor world-space labels with the same text and locale binding model. font = "res://fonts/Game.ttf"uses an imported.ttf,.otf, or.ttcfont.font = "system://Segoe UI"requests a known system font.- Missing or invalid fonts fall back to the default font chain.
- Known system names:
SansSerif,Serif,Monospace,Arial,Calibri,Cambria,Consolas,Courier New,Georgia,Helvetica,Segoe UI,Times New Roman,Verdana. - System fonts vary by OS. Use
res://for stable game visuals.
UiTextBox
- Single-line text input.
- Holds
text,placeholder, text colors,text_size_ratio,style,focused_style,editable, andinput_type. - Emits hover / focus /
text_changedsignals (see text-edit signals near the end of this page). - Use it for name fields, search bars, and numeric entry.
UiTextBlock
- Multi-line text input.
- Same fields and signals as
UiTextBox, but wraps and keeps newlines. - Use it for chat input, notes, and multi-line forms.
UiScrollContainer
- Invisible clipped UI container with scroll offset.
- Holds
scroll/scroll_x/scroll_y. - Offsets child content and clips descendants to its rect.
- Wheel targets hovered scroller.
- Keyboard targets focused-node ancestor scroller or sole visible root scroller.
- Key map:
ArrowUp,ArrowDown,PageUp,PageDown,Home,End. scroll_to(part, duration)scrolls to normalized content part.part:0.0= start,0.5= middle,1.0= end.duration: seconds;0.0snaps.- Example:
with_node_mut!(ctx.run, UiScrollContainer, list, |node| node.scroll_to(0.0, 0.25)); - Current v1 target = desktop wheel + keyboard.
- Touch / drag scroll path defer.
UiLayout
- Invisible layout container.
- Uses
mode = "h" | "v" | "grid". - Also accepts
horizontal,vertical,row,column, andg.
UiHLayout
- Invisible horizontal layout container.
- Presets
mode = "h".
UiVLayout
- Invisible vertical layout container.
- Presets
mode = "v".
UiGrid
- Invisible grid layout container.
- Uses
columns,h_spacing, andv_spacing.
UiTreeList
- Data-driven nested list.
- Holds
itemson node instead of one child node per row. - Each item has
label,id,value,parent,open, andselectable. - Uses built-in row buttons, triangle toggles, optional icons, labels, and branch guide lines.
- Emits
selected_signalsandtoggled_signals. - Uses
indent,row_height,v_spacing,icon_size, andtoggle_size.
Tree list:
[rows]
[UiTreeList]
size_ratio = (1.0, 1.0)
row_height = 24.0
indent = 18.0
v_spacing = 0.006
items = [
{ id = "root", label = "Node3D", open = true },
{ id = "mesh", label = "MeshInstance3D", parent = 0 },
]
[/UiTreeList]
[/rows]
Common fields live on UiNode data and all UI nodes inherit them:
anchorsize_percentsize_ratiopivot_percentpivot_ratiotranslation_percenttranslation_ratioself_translation_percentself_translation_ratioscaleh_sizev_sizeh_alignv_alignmin_size_ratiomax_size_ratiopaddingmarginz_indexvisibleinput_enabledmouse_filterclip_children
Anchors:
c center
l left
r right
t top
b bottom
tl top_left
tr top_right
bl bottom_left
br bottom_right
Default anchor is center.
Default translation_ratio and self_translation_ratio are (0.0, 0.0).
Default h_align and v_align are center.
Default label text align is center.
Default clip_children is false (children may render outside parent bounds).
Set clip_children = true to clip descendants to that node rect.
corner_radius and radius use 0.0..1.0.
0.0 means square corners.
1.0 means half of the shortest side.
"full" also means 1.0.
corner_radii = (tl, tr, br, bl) sets each corner separately.
radius_tl, radius_tr, radius_br, and radius_bl override corners one by one.
fill_kind = "solid" | "linear" swaps between flat fill and built-in gradient fill.
gradient = { start_color, end_color, vector } defines linear gradient fill.
shadow maps to outer_shadow.
highlight maps to inner_highlight.
outer_shadow, inner_shadow, outer_highlight, and inner_highlight add depth to UiPanel, UiButton, UiTextBox, and UiTextBlock.
Each accepts color, distance, falloff, vector, and size.
vector is the light/shadow direction in UI space.
falloff approximates blur with layered alpha.
size is relative to the panel/button size.
size = 1 matches the panel/button.
size = 2 doubles it.
size = 0.5 halves it.
UI layout always resolves from a parent rect.
- Root UI node parent = virtual viewport.
- Child UI node parent = closest UI ancestor.
- Non-UI wrapper nodes do not create UI layout space.
visible = falsehides the node and its UI descendants.- Showing the parent again makes descendants render on the next UI extract.
create_node!,create_nodes!, andreparent!mark UI layout/render dirty.
Use force_rerender! only when code bypasses the normal mutation APIs and edits hidden/visible state directly.
Normal with_node_mut!, with_base_node_mut!, create_node!, create_nodes!, and reparent! calls do not need it.
UiHLayout, UiVLayout, and UiLayout stack fixed-size children in order.
No shrink-to-fit.
Each child keeps its own size_ratio.
padding insets the container content rect first.
Spacing is a ratio of that content size.
h_spacing uses content width.
v_spacing uses content height.
Main-axis budget for n children:
Ξ£ child size_ratio + spacing Γ (n β 1).
If that sum exceeds 1.0, children overflow the container.
clip_children defaults to false, so overflow renders outside the parent and can leave the screen.
h_align (for h layouts) and v_align (for v layouts) only place the packed run when it is smaller than the content rect.
On the main axis, fill align acts like start; it does not stretch fixed children.
Absorb variable or animated content with fill children, not overflow.
h_size = "fill" and v_size = "fill" children split the leftover:
fill = (available β fixed β spacing) / fill_count.
Worked column example, 6 rows in a UiVLayout with v_spacing = 0.018:
- 4 fixed rows,
size_ratioheight0.15each =>0.60total. - spacing =
0.018 Γ 5gaps =0.09. - leftover =
1.0 β 0.60 β 0.09=0.31. - 2
v_size = "fill"rows share it =>0.155each.
Fixed rows summing past 1.0 β spacing push the last rows off the container.
A fill row absorbs a changing fixed row without breaking the budget.
Scene UI uses ratio/percent fields. Think parent first, then node.
size_ratio = (0.5, 0.25)=> node size = 50% parent width and 25% parent height.pivot_ratio = (0.5, 0.5)=> pivot at node center.pivot_ratio = (0.0, 1.0)=> pivot at node top-left.translation_ratio = (1.0, 0.0)=> move right by one parent width after layout.translation_ratio = (0.0, 1.0)=> move up by one parent height after layout.translation_ratio = (0.0, -0.5)=> move down by half parent height after layout.self_translation_ratio = (1.0, 0.0)=> move right by one own width after size resolves.self_translation_ratio = (0.0, 1.0)=> move up by one own height after size resolves.
Anchor pins the matching node edge/corner/center to the parent anchor.
Pivot chooses rotate/scale origin inside that already placed node.
Pivot does not choose where the node is placed.
Translation moves after layout by parent size.
Self translation moves after layout by node size.
Scene position_ratio, position_percent, and position_pct are ignored legacy fields.
Common anchor results:
anchor = "center"+ no translation => centered.anchor = "tr"+ no translation => node top-right corner sits on parent top-right corner.anchor = "bl"+ no translation => node bottom-left corner sits on parent bottom-left corner.anchor = "b"+ no translation => node bottom edge sits on parent bottom edge.anchor = "top"+self_translation_ratio = (0.0, -0.5)=> move down by half node height after top edge placement.
Pivot example:
anchor = "b"+pivot_ratio = (0.5, 0.5)+ node height100=> pivot is 50 above parent bottom.anchor = "b"+pivot_ratio = (0.5, 1.0)+ node height100=> pivot is 100 above parent bottom.- In both cases, no translation means the node bottom edge stays on the parent bottom edge.
Use one of 9 anchors for base placement.
Then use translation_ratio = (x, y) for parent-space movement after layout.
Use self_translation_ratio = (x, y) for own-size movement after layout.
x > 0 moves right.
x < 0 moves left.
y > 0 moves up.
y < 0 moves down.
tl t tr
l c r
bl b br
Example horizontal placement:
anchor = "c"+translation_ratio = (0.25, 0.0)reaches midpoint between center and right edge.anchor = "r"+translation_ratio = (-0.125, 0.0)reaches the same point.
Example vertical placement:
anchor = "c"+translation_ratio = (0.0, 0.25)reaches midpoint between center and top edge.anchor = "t"+translation_ratio = (0.0, -0.125)reaches the same point.
These pairs match because translation_ratio moves by the parent size.
If resolved node size changes, translation values that hit the same parent-space point stay stable.
Inline style blocks are the base schema:
style = { fill = "#222" stroke = "#555" radius = 0.2 }
The same schema loads from a .uistyle resource:
style = "res://ui/panel.uistyle"
Button state styles take resources:
hover = { style = "res://ui/button_hover.uistyle" }
pressed = { style = "res://ui/button_down.uistyle" }
Text edit focus takes a resource:
focused_style = "res://ui/input_focus.uistyle"
UiPanel, UiButton, UiCheckbox, UiDropdown, UiColorPicker, UiTextBox, and UiTextBlock accept style as an inline block or a .uistyle path.
Both forms ship today; inline and resource styles are interchangeable.
.uistyle is visual-only.
It mirrors UiStyle fields such as fill, stroke, stroke_width, radius, shadow, and highlight.
It does not define layout, classes, or global themes.
See .uistyle Format.
UI space uses center origin. Top-level UI nodes use the virtual viewport as parent. Children use parent UI rect as parent.
pivot_ratio = (0.5, 0.5) means pivot at node center.
Pivot affects rotation/scale origin, not final anchor placement.
Anchor placement pins node edge/corner/center to the matching parent point before translation.
translation_ratio = (x, y) offsets by parent size.
self_translation_ratio = (x, y) offsets by own resolved size.
Example: translation_ratio = (0.0, 0.5) moves node up by half parent height.
Example: self_translation_ratio = (-1.0, 0.0) moves node left by one own width.
scale multiplies final clamped size.
h_size and v_size accept fixed, fill, or fit_children.
h_align accepts start, center, end, or fill.
v_align accepts start, center, end, or fill.
size_ratio always resolves against current parent size (or root viewport size), ensuring no matter the screen size, the game looks the same without thinking of absolute sizing.
min_size_ratio and max_size_ratio clamp against node baseline size.
Baseline size = node resolved size at spawn/creation time.
If size definition changes later (size_ratio, size mode), baseline rebases to new resolved size.
min_size_ratio = (1.0, 1.0) + max_size_ratio = (1.0, 1.0) locks node at spawn-relative size, since it can only be 100% of it's creation size.
min_size_ratio = (0.8, 0.8) + max_size_ratio = (1.2, 1.2) allows small dynamic scale band up and down for changing window size, but not so unruly that it compresses or enlarges.
Layout spacing keys are ratio-based:
spacing, h_spacing resolve against parent content width.
v_spacing resolves against parent content height.
Example: grid size_ratio = (1, 1) + h_spacing = 0.25 => horizontal gap = 25% of container width.
Layout padding is ratio-based:
left/right resolve against own width.
top/bottom resolve against own height.
padding = 0.5 uses half size on each side.
Absolute UI keys unsupported in scenes:
position, pivot, translation, size, size_px, pixel_size, min_size, max_size, min_w, min_width, min_h, min_height, max_w, max_width, max_h, max_height, font_size.
Legacy position_ratio, position_percent, and position_pct are accepted but ignored.
Use ratio/percent keys + text_size_ratio.
Example:
[menu]
[UiNode]
anchor = "tr"
size_ratio = (0.15625, 0.185185)
padding = 0.08
[/UiNode]
[/menu]
Full-screen root + centered panel:
[ui_root]
[UiNode]
anchor = "center"
size_ratio = (1.0, 1.0)
pivot_ratio = (0.5, 0.5)
[/UiNode]
[/ui_root]
[card]
parent = @ui_root
[UiPanel]
anchor = "center"
size_ratio = (0.45, 0.35)
pivot_ratio = (0.5, 0.5)
style = { fill = "#20242C" stroke = "#586070" radius = 0.12 }
[/UiPanel]
[/card]
Top-right HUD:
[hud_stats]
[UiPanel]
anchor = "tr"
size_ratio = (0.18, 0.10)
pivot_ratio = (0.5, 0.5)
translation_ratio = (-0.15, -0.15)
style = { fill = "#111827CC" stroke = "#93A4B8" radius = 0.15 }
[/UiPanel]
[/hud_stats]
Bottom-left button row:
[quick_bar]
[UiHLayout]
anchor = "bl"
size_ratio = (0.28, 0.08)
pivot_ratio = (0.5, 0.5)
translation_ratio = (0.15, 0.15)
h_spacing = 0.04
[/UiHLayout]
[/quick_bar]
[slot_1]
parent = @quick_bar
[UiButton]
size_ratio = (0.22, 0.8)
style = { fill = "#263238" stroke = "#90A4AE" radius = 0.18 }
[/UiButton]
[/slot_1]
Spawn UI from script:
methods!({
fn spawn_toast(&self, ctx: &mut ScriptContext<'_, API>, parent: NodeID) {
let panel = create_node!(ctx.run, UiPanel);
let _ = with_node_mut!(ctx.run, UiPanel, panel, |node| {
node.base.layout.anchor = UiAnchor::Top;
node.base.layout.size = UiVector2::ratio(0.32, 0.08);
node.base.transform.translation = Vector2::new(0.0, -0.75);
node.style.fill = Color::new(0.05, 0.06, 0.08, 0.92);
});
let _ = reparent!(ctx.run, parent, panel);
}
});If low-level code edits UI data without with_node_mut!, force subtree extraction:
let _ = force_rerender!(ctx.run, ui_root);Button state example:
[play_button]
[UiButton]
size_ratio = (0.114583, 0.044444)
# Extra signals. Default signals still emit:
# play_button_pressed
# play_button_clicked
pressed_signals = ["play_down"]
clicked_signals = ["play_clicked", "any_button_clicked"]
style = {
fill = "#344E41"
stroke = "#A3B18A"
radius = 0.3
shadow = { color = "#00000066" distance = 10 falloff = 12 vector = (1, -1) size = 2 }
highlight = { color = "#FFFFFF55" distance = 2 falloff = 3 vector = (-1, 1) size = 2 }
}
hover = {
scale = (1.02, 1.02)
rotation = 0.02
style = { fill = "#3A5A40" stroke = "#DAD7CD" radius = 0.35 }
}
pressed = {
scale = (0.98, 0.98)
rotation = -0.01
style = { fill = "#1B4332" stroke = "#95D5B2" radius = 0.25 }
}
[/UiButton]
[/play_button]
Animated image example:
[coin_icon]
[UiAnimatedImage]
texture = "res://ui/coin_strip.png"
size_ratio = (0.04, 0.07)
scale_mode = "fit"
animations = [
{ name = "spin" start = (0, 0) frame_size = (32, 32) frame_count = 8 columns = 8 fps = 12 },
]
animation = "spin"
playing = true
looping = true
[/UiAnimatedImage]
[/coin_icon]
Image button example:
[play_icon]
[UiImageButton]
texture = "res://ui/play.png"
size_ratio = (0.08, 0.08)
scale_mode = "fit"
# Extra signal. Default play_icon_clicked still emits.
clicked_signals = ["play_clicked"]
hover = { scale = (1.06, 1.06) tint = "#FFFFFFFF" }
pressed = { scale = (0.94, 0.94) tint = "#CCCCCCFF" }
[/UiImageButton]
[/play_icon]
Nine-slice example:
[dialog_panel]
[UiNineSlice]
texture = "res://ui/panel.png"
texture_region = (0, 0, 64, 64)
size_ratio = (0.45, 0.30)
tint = "#FFFFFFFF"
[/UiNineSlice]
[/dialog_panel]
Default zero margins split the full texture or region into equal thirds.
Corners stay fixed while edges and center tile to fit the resolved UI rect.
Set nonzero margins as left, top, right, bottom source pixels for custom fixed corners.
Old flat button color fields still work:
fill = "#344E41"
hover_fill = "#3A5A40"
pressed_fill = "#1B4332"
UiButton, UiImageButton, Button2D, and ImageButton2D emit these events:
hover_enter
hover_exit
pressed
released
clicked
All button nodes use pointer cursor by default while hovered.
Set cursor_icon or hover_cursor_icon to override it.
Use "pointer" for hand pointer.
"hand" is accepted as an alias.
Each event always emits its default named signal:
<button_node_name>_hover_enter
<button_node_name>_hover_exit
<button_node_name>_pressed
<button_node_name>_released
<button_node_name>_clicked
This is the NAME_ACTION rule.
Node play_button click emits play_button_clicked even when clicked_signals is empty.
Custom signal fields are extra signals. They add signals on top of the default named signal:
hover_signals = ["play_hover"]
hover_exit_signals = ["play_unhover"]
pressed_signals = ["play_down"]
released_signals = ["play_up"]
clicked_signals = ["play_clicked", "any_button_clicked"]
All handlers receive (button: NodeID).
Keyboard/controller focus uses the same button hover/press visual states and events.
Tab moves focus forward.
Shift+Tab moves focus backward.
Gamepad D-pad and left stick move focus toward the nearest control in that direction.
Joy-Con stick uses the same directional focus path.
Enter, Space, gamepad bottom face button, and Joy-Con right face button activate the focused button.
Buttons and text edits can filter focus/activation input by player or device id.
Use input_only_*/input_allow_* fields for allow lists.
Use input_block_*/input_deny_* fields for deny lists.
Deny wins if the same source matches both.
If any allow list is set, unmatched sources are ignored.
input_only_players = [0]
input_block_gamepads = [1]
input_only_joycons = [0, 1]
input_allow_kbm = true
input_deny_kbm = true
Full scene mask examples:
[p1_start]
[UiButton]
text = "Start"
input_only_players = [0]
input_block_gamepads = [1]
[/UiButton]
[/p1_start]
[joycon_name]
[UiTextBox]
input_only_joycons = [0, 1]
input_block_players = [2]
[/UiTextBox]
[/joycon_name]
[keyboard_help]
[UiTextBlock]
text = "Press Space"
input_allow_kbm = true
input_deny_gamepads = [0, 1, 2, 3]
[/UiTextBlock]
[/keyboard_help]
Runtime script mask example:
let _ = with_node_mut!(ctx.run, UiButton, play_button, |button| {
button.input_mask.allow_players = vec![0];
button.input_mask.deny_gamepads = vec![1];
});
let _ = with_node_mut!(ctx.run, UiTextBox, name_field, |field| {
field.inner.input_mask.allow_joycons = vec![0, 1];
field.inner.input_mask.deny_players = vec![2];
});
let _ = with_node_mut!(ctx.run, UiTextBlock, help_text, |text| {
text.inner.input_mask.allow_kbm = true;
});Scene example:
[play_button]
[UiButton]
size_ratio = (0.114583, 0.044444)
# Extra signals only. Default play_button_* signals still emit.
hover_signals = ["menu_button_hover"]
pressed_signals = ["play_down", "any_button_down"]
released_signals = ["play_up"]
clicked_signals = ["play_clicked", "any_button_clicked"]
[/UiButton]
[/play_button]
Connect to named and custom signals:
lifecycle!({
fn on_all_init(&self, ctx: &mut ScriptContext<'_, API>) {
signal_connect_many!(
ctx.run,
ctx.id,
[
signal!("play_button_hover_enter"),
signal!("play_button_hover_exit"),
signal!("play_button_pressed"),
signal!("play_button_released"),
signal!("play_button_clicked"),
signal!("menu_button_hover"),
signal!("play_down"),
signal!("any_button_down"),
signal!("play_up"),
signal!("play_clicked"),
signal!("any_button_clicked"),
],
[func!("on_button")]
);
}
});
methods!({
// pub because signal dispatch only reaches pub fn methods.
pub fn on_button(&self, ctx: &mut ScriptContext<'_, API>, button: NodeID) {
println!("button={button:?}");
}
});Runtime add/remove custom emits:
let _ = with_node_mut!(ctx.run, UiButton, play_button, |button| {
let sig = signal!("debug_play_click");
if !button.clicked_signals.contains(&sig) {
button.clicked_signals.push(sig);
}
button.pressed_signals.retain(|s| *s != signal!("old_press_signal"));
});UiTextBox and UiTextBlock emit these events:
hovered
unhovered
focused
unfocused
text_changed
Each event always emits its named signal:
<text_node_name>_hovered
<text_node_name>_unhovered
<text_node_name>_focused
<text_node_name>_unfocused
<text_node_name>_text_changed
Custom signal fields add signals on top of named signals:
hover_signals = ["name_hover"]
hover_exit_signals = ["name_unhover"]
focused_signals = ["name_focus"]
unfocused_signals = ["name_unfocus"]
text_changed_signals = ["name_changed"]
Hover/focus handlers receive (text_edit: NodeID).
Text change handlers receive (text_edit: NodeID, text: String).
Scene example:
[name_input]
[UiTextBox]
hover_signals = ["name_hover"]
hover_exit_signals = ["name_unhover"]
focused_signals = ["name_focus"]
unfocused_signals = ["name_unfocus"]
text_changed_signals = ["name_changed"]
[/UiTextBox]
[/name_input]
[bio_input]
[UiTextBlock]
text_changed_signals = ["bio_changed"]
[/UiTextBlock]
[/bio_input]