Skip to content

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.

RuleDetail
Larger valueDrawn in front of smaller values
Key namez_index or zIndex (either form on props)
Same z_indexOrder follows sibling index after the batch (upsert / append order; pre–z_index behavior)
ScopePer 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.).

lua
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" }
})
PropDescription
textText content (supports TMP rich text)
StyleDescription
font_sizeFont size (default 16)
colorText 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).

lua
ui:element({
    id = "bg", type = "panel",
    rect = { unit = "px", x = 0, y = 0, w = 480, h = 272 },
    style = { bg = "#0F172A" }
})
StyleDescription
bgBackground color
gradientEnd 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.

PropDescription
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_frontDefault "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_groupComma- 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_boundsOptional 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) during rebuild() to place the panel at the user's chosen position.
  • surface:on_drag(fn) to know when a gesture finished so you can re-run rebuild(). The callback receives the dragged element's id.
  • surface:on_move(fn) - convenience: registers both on_drag and (on visor surfaces) ss.hud.on_overlay_change in a single call. Replace both separate registrations with hud:on_move(rebuild_shell).
  • surface:set_drag_offset(id, dx, dy) to restore a saved position (call at module level before the first rebuild(), using values loaded from ic.persist).
  • surface:is_dragging([id]) - returns true while a panel-repositioning drag gesture is live. Omit id to test any element on the surface; pass a specific id to test one panel. Use this in tick() to skip rebuilds that would snap the panel back to its committed (pre-drag) position:
    lua
    function tick(dt)
      -- accumulate time ...
      if not hud:is_dragging() then
        rebuild_shell()
      end
    end
  • handle: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).

PropDescription
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_hMinimum size in canvas pixels (default 40).
resize_max_w, resize_max_hMaximum size in canvas pixels (default 4096).

Reading the resized dimensions. The runtime stores the committed size for each resizable leader:

  • surface:panel_size(id) during rebuild() - returns {w=n, h=n} when a size has been committed, else nil.
  • surface:on_resize(fn) - callback when a resize gesture completes (receives the element id).
  • 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 from ic.persist before the first rebuild.
  • surface:is_resizing([id]) - returns true while a resize gesture is live. Guard tick() rebuilds alongside is_dragging():
    lua
    if not hud:is_dragging() and not hud:is_resizing() then
      rebuild_shell()
    end
  • handle: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).

PropElementsDescription
drag_sourcepanel, 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_targetpanel, 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).

lua
-- 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" },
})
PropDescription
valueCurrent value (ignored in indeterminate mode)
minMinimum value (default 0)
maxMaximum value (default 1)
indeterminateIf true, shows animated sliding fill
directionFill 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.
StyleDescription
bgTrack background color
fillBase fill color
color_stopsArray of { fraction, "#color" } — fill color changes at thresholds
speedAnimation speed for indeterminate mode (default 1.2)

divider ​

Horizontal or vertical separator line.

lua
ui:element({
    id = "sep", type = "divider",
    rect = { unit = "px", x = 10, y = 50, w = 460, h = 1 },
    style = { color = "#334155" }
})
StyleDescription
colorDivider color (default gray)

⚠️ This documentation was AI-generated and may contain inaccuracies. Please submit pull requests with corrections as needed.