Display Elements
Draw order (z_index)
All element types can use z_index or zIndex on props only to control stacking among siblings (widgets that share the same parent in the UI tree: surface root, a nested container’s children, or scroll view content). Visual styling stays in style; draw order is always a prop so authors are not split between two tables.
| Rule | Detail |
|---|---|
| Larger value | Drawn in front of smaller values |
| Key name | z_index or zIndex (either form on props) |
Same z_index | Order follows sibling index after the batch (upsert / append order; pre–z_index behavior) |
| Scope | Per parent only — z on a child does not reorder against cousins under another parent |
Order is applied after each UI sync batch (e.g. after ui:commit() or tick-driven updates). Overlapping labels and panels both participate.
For a full explanation and nested examples, see Elements — Draw order. Shipped regression demo: Examples/ZIndexDemo.lua.
label
Text display. Supports TMP rich text tags (<b>, <color>, etc.).
ui:element({
id = "title", type = "label",
rect = { unit = "px", x = 20, y = 10, w = 200, h = 30 },
props = { text = "Hello World" },
style = { font_size = 18, color = "#FFFFFF", align = "left" }
})| Prop | Description |
|---|---|
text | Text content (supports TMP rich text) |
| Style | Description |
|---|---|
font_size | Font size (default 16) |
color | Text color |
align | "left", "center", "right" |
panel
Background rectangle. By default the panel image does not receive pointer hits (clicks pass through to widgets behind). Optional draggable mode turns on hit-testing on the panel so you can drag a HUD cluster or reposition a chrome region (works on motherboard ss.ui, tablet cartridge ss.ui, and visor ss.hud — same props everywhere).
ui:element({
id = "bg", type = "panel",
rect = { unit = "px", x = 0, y = 0, w = 480, h = 272 },
style = { bg = "#0F172A" }
})| Style | Description |
|---|---|
bg | Background color |
gradient | End color for gradient (start = bg), array of colors, or array of {position, color} pairs |
gradient_dir | "horizontal", "vertical" (default), "diagonal", "radial" |
There is no border, border_width, or border_radius style on panel in the renderer. For a colored frame, add a rect_outline (or border) element with the same rect and style.color / style.thickness (default thickness 2).
Draggable panels
Set props.draggable to "true" or "1" on a type = "panel" element. While dragging, every listed drag_group widget moves together; on pointer release, their rectangles are written back to the authoritative UI model and synced like any other layout change.
| Prop | Description |
|---|---|
draggable | "true" / "1" - panel receives pointer drag; raise z_index on buttons, interface_button, and other controls above this panel so they stay clickable. |
drag_to_front | Default "true". When enabled, grabbing this panel raises its z_index above same-parent siblings (desktop-window stacking). Set "false" or "0" to keep manual z_index control. |
drag_group | Comma- or semicolon-separated element ids on the same surface that move with this drag (default: this panel's id only). Use this for sibling panels that should translate together but are not parent/child in the layout tree. For surface:layout(...) containers, "auto" or drag_group_auto = true marks the draggable leader as a unit - layout children are parented under it via parent_id and follow automatically (do not list descendants manually). Every id in the group must use rect.unit = "px". |
drag_bounds | Optional live clamp for the draggable leader during the gesture. "screen" / "surface" keeps the leader fully inside the surface root. "element:<id>" keeps the leader fully inside another same-surface element's rect. Followers in drag_group move with the leader. Omit the prop for raw unclamped drag. |
Reading the dragged position. The runtime stores the cumulative canvas-pixel offset for each draggable leader. Use:
surface:drag_offset(id)duringrebuild()to place the panel at the user's chosen position.surface:on_drag(fn)to know when a gesture finished so you can re-runrebuild(). The callback receives the dragged element'sid.surface:on_move(fn)- convenience: registers bothon_dragand (on visor surfaces)ss.hud.on_overlay_changein a single call. Replace both separate registrations withhud:on_move(rebuild_shell).surface:set_drag_offset(id, dx, dy)to restore a saved position (call at module level before the firstrebuild(), using values loaded fromic.persist).surface:is_dragging([id])- returnstruewhile a panel-repositioning drag gesture is live. Omitidto test any element on the surface; pass a specificidto test one panel. Use this intick()to skip rebuilds that would snap the panel back to its committed (pre-drag) position:luafunction tick(dt) -- accumulate time ... if not hud:is_dragging() then rebuild_shell() end endhandle:on("drag_begin", fn)/handle:on("drag_end", fn)for per-element drag notifications when you want them at the element rather than surface level.
See Programmable Visor HUDs - Dragging visor panels for the full pattern and Examples/DragBoundsDemo.lua for handle:on("drag_begin", fn) element callbacks.
Multiplayer (drag): drag_end uses the same serialized UI-input path as clicks (DispatchUiInput -> host). The host applies the gesture delta to the authoritative model, updates the stored cumulative offset, and flushes; pure clients mirror live RectTransform positions into the local element dictionary (no pending ops) so ApplyRect does not snap the group back before the next sync.
Resizable panels
Set props.resizable on a type = "panel" element to show a SE-corner resize grip. Only the leader panel's width/height change during the gesture; layout children follow via parent_id hierarchy (same rule as drag - do not list descendants in a resize group).
| Prop | Description |
|---|---|
resizable | "true", "1", "se", "both" - width + height from SE corner. "e" / "s" - width or height only. Comma-separated "e,s" also accepted. |
resize_min_w, resize_min_h | Minimum size in canvas pixels (default 40). |
resize_max_w, resize_max_h | Maximum size in canvas pixels (default 4096). |
Reading the resized dimensions. The runtime stores the committed size for each resizable leader:
surface:panel_size(id)duringrebuild()- returns{w=n, h=n}when a size has been committed, elsenil.surface:on_resize(fn)- callback when a resize gesture completes (receives the elementid).surface:on_layout(fn)- convenience on visor: registers callbacks for drag-end, resize-end, and overlay change in one call. On board/cartridge: drag + resize only.surface:set_panel_size(id, w, h)- restore a saved size fromic.persistbefore the first rebuild.surface:is_resizing([id])- returnstruewhile a resize gesture is live. Guardtick()rebuilds alongsideis_dragging():luaif not hud:is_dragging() and not hud:is_resizing() then rebuild_shell() endhandle:on("resize_begin", fn)/handle:on("resize_end", fn)for per-element resize notifications.
Example: Examples/VisorHudResizeDemo.lua.
Multiplayer: resize_end uses the same UI-input path as drag. The host applies dw&dh to the authoritative model, stores PanelSizes, and flushes; pure clients mirror live sizeDelta into the local element dictionary before sync arrives.
set_props / set_style during resize: while a resize gesture is active, ApplyRect is skipped for the leader RectTransform (same mid-gesture immunity as drag).
set_props / set_style during drag: property-only handle updates merge from the model. While a drag_group pointer drag or resize gesture is active, ApplyRect is skipped for those live RectTransforms so a concurrent flush cannot reset mid-gesture positions.
Bounds are still script-author owned by default: the runtime does not clamp drag unless you opt in with drag_bounds. That means off-edge peek, overscroll feedback, or larger virtual spaces still work without the engine snapping your panel back. If you do want a HUD cluster to stop at the visible edge while dragging, set drag_bounds = "screen" (or "surface"). See Examples/DragBoundsDemo.lua for a minimal example and Examples/VisorHudPong.lua for a full visor HUD using the same pattern.
Drag payload & drop targets (uGUI drag-and-drop)
Separate from panel reposition drags: you can mark panel, button, or icon elements as payload sources and other panel / button / icon elements as drop zones (Unity IDropHandler). Use this for “drag this ingot thumbnail into that silo slot” style UX. While dragging, the engine spawns a semi-opaque duplicate of the source graphic (sprite or RawImage texture) parented to the same canvas so it follows the pointer; the source slot dims until release. Example: Examples/DragDropSilo.lua (full-surface ss.ui demo with per-silo “last accepted” icons).
| Prop | Elements | Description |
|---|---|---|
drag_source | panel, button, icon | "true" / "1" — this widget starts a payload drag (not drag_group movement). Requires drag_payload. Mutually exclusive with draggable on the same panel: if both are set, drag_source wins (no ScriptedScreensUiDragGroup on that element). |
drag_payload | (with drag_source) | Non-empty string token carried to the drop handler (e.g. prefab name ItemSteelIngot). Avoid & in the token; drop value uses & as delimiter. |
drop_target | panel, button, icon | "true" / "1" — accepts drops from the same chip + surface. The panel/image must receive raycasts (drop_target forces hit-testing on panel when it would otherwise be transparent to hits). |
drop_accepts | (with drop_target) | Optional whitelist of allowed drag_payload values (case-insensitive). Prefer a Lua array of strings, e.g. { "ItemSteelIngot", "ItemGoldIngot" }. Legacy: a single comma/semicolon-separated string is still parsed. Omit or leave empty to accept any non-empty payload. |
drop_dispatch_id | (with drop_target) | Optional. poll_input ev.id for successful drop events (defaults to the drop zone element’s id). |
poll_input: drop — id = drop_dispatch_id or drop zone id; value = "<payload>&<source_element_id>&<target_element_id>". Optional: drag_payload_begin / drag_payload_cancel on the source element id ( value = payload; cancel fires if the pointer released without a valid drop).
Multiplayer: drag_payload_begin, drag_payload_cancel, and drop are sent over the same UI-input channel. The host re-validates each drop (source drag_payload, target drop_accepts, ids) before DispatchInputEvent so clients cannot forge drops.
progress
Progress bar with filled portion. Supports color stops, indeterminate mode, and configurable fill direction (horizontal or vertical).
-- Basic progress bar
ui:element({
id = "hp", type = "progress",
rect = { unit = "px", x = 10, y = 10, w = 200, h = 20 },
props = { value = 75, min = 0, max = 100 },
style = { bg = "#1E293B", fill = "#22C55E" },
})
-- With color stops (fill changes color based on value)
ui:element({
id = "temp", type = "progress",
rect = { unit = "px", x = 10, y = 40, w = 200, h = 20 },
props = { value = 0.7 },
style = {
bg = "#1E293B",
fill = "#22C55E",
color_stops = {
{ 0.5, "#EAB308" }, -- yellow from 50%
{ 0.8, "#EF4444" }, -- red from 80%
},
},
})
-- Indeterminate (animated sliding bar)
ui:element({
id = "loading", type = "progress",
rect = { unit = "px", x = 10, y = 70, w = 200, h = 8 },
props = { indeterminate = true },
style = { bg = "#1E293B", fill = "#38BDF8" },
})
-- Vertical progress bar (bottom to top)
ui:element({
id = "fuel_level", type = "progress",
rect = { unit = "px", x = 10, y = 100, w = 30, h = 120 },
props = { value = 60, min = 0, max = 100, direction = ss.direction.BottomToTop },
style = { bg = "#1E293B", fill = "#38BDF8" },
})
-- Right-to-left progress bar
ui:element({
id = "rtl_bar", type = "progress",
rect = { unit = "px", x = 50, y = 100, w = 200, h = 20 },
props = { value = 0.4, direction = ss.direction.RightToLeft },
style = { bg = "#1E293B", fill = "#EAB308" },
})| Prop | Description |
|---|---|
value | Current value (ignored in indeterminate mode) |
min | Minimum value (default 0) |
max | Maximum value (default 1) |
indeterminate | If true, shows animated sliding fill |
direction | Fill direction: ss.direction.LeftToRight (default), ss.direction.RightToLeft, ss.direction.TopToBottom, ss.direction.BottomToTop. Raw strings "ltr", "rtl", "ttb", "btt" also accepted. Works with both determinate and indeterminate modes. |
| Style | Description |
|---|---|
bg | Track background color |
fill | Base fill color |
color_stops | Array of { fraction, "#color" } — fill color changes at thresholds |
speed | Animation speed for indeterminate mode (default 1.2) |
divider
Horizontal or vertical separator line.
ui:element({
id = "sep", type = "divider",
rect = { unit = "px", x = 10, y = 50, w = 460, h = 1 },
style = { color = "#334155" }
})| Style | Description |
|---|---|
color | Divider color (default gray) |
