DEMIENGINEDOCUMENTATION Everything is free
Create

In this guide

Retained HUD trees, layout, themes, localization, prefabs, and accessibility.

UI & HUD

Runtime UI is a retained tree authored in *.hud.json documents. You describe the layout once; scripts mutate text, values, and visibility at runtime. The editor HUD stage edits the same tree the runtime renders.

A HUD menu: Play/Settings/Inventory buttons beside a settings panel with a dropdown, slider, and progress bars.
A HUD tree at runtime: a column of buttons beside a settings panel with a dropdown, a slider, and progress bars — all authored in *.hud.json.

Document shape

{
  "format_version": 1,
  "canvas_size": [1280, 720],
  "theme": "../ui/themes/dark.json",
  "localization_file": "../ui/en.localization.json",
  "variables": ["game_title"],
  "action_map": { "submit": "confirm" },
  "action_effects": {
    "menu_accept": { "focus": "start" }
  },
  "root": {
    "id": "menu",
    "type": "container",
    "dock": "fill",
    "stack": "column",
    "pad": 24,
    "gap": 12,
    "children": [
      { "id": "title", "type": "label", "text": "${game_title}", "font_size": 32 },
      { "id": "start", "type": "button", "text": "Start", "action": "menu_accept" },
      { "id": "options", "type": "button", "text": "Options", "action": "menu_options" }
    ]
  }
}

Walkthrough: canvas_size is the design resolution every position and size is measured against. theme and localization_file are paths relative to the HUD file. root is a full-screen column with 24 units of padding and 12 units between children. The title interpolates ${game_title}; the top-level variables array declares variable names — never values — and Hud.set_variable fills them at runtime. A button's action names the UI action it emits. action_effects can focus or show/hide nodes when that UI action fires. action_map maps navigation slots (next, previous, submit, cancel) to input action names.

You can omit root and write top-level children instead — an implicit fill container is inserted. Prefer dock, stack, pad, and at before raw anchors.

Node types

TypeRoleUseful fields
container, panelLayout and groupingstack, gap, alignment, columns
label, textStatic / editable texttext, font_size, text_wrap, text_alignment
imageTexture displaytexture (asset://…), animation frames
button, toggle, slider, text_inputInteractive controlsaction, disabled, focusable, placeholder; slider takes minimum/maximum (defaults 0 and 1)
scroll, list, progressScrolling, lists, progresschildren / value
modalFocus-trapping dialogchildren
virtual_button, virtual_stickTouch controlscontrol (publishes virtual:*)

Layout

  • Every node accepts anchor_min/anchor_max, margin, padding, min_size, max_size.
  • stack is row, column, or grid, with alignment (start|center|end|stretch), gap, and columns.
  • dock pins a node: fill|top|bottom|left|right|center.
  • at places a node at an explicit canvas point; size sets its extent in canvas units.
  • Safe areas protect readable and interactive content by default. Set respect_safe_area: false only for intentional edge-to-edge backgrounds.

Colors accept a [r, g, b, a] array (0…1) or a hex string like #1b1c27cc.

Text, fonts, and localization

  • "font": "asset://fonts/body" picks a Font2D asset. The default is embedded Inter Variable.
  • text_wrap: none|word|grapheme. text_overflow: visible|clip|ellipsis.
  • text_alignment / text_vertical_alignment: start|center|end. max_lines caps the block.
  • Text.parse_rich can parse [color], [em], [strong], [link], and [icon] into spans for scripts; HUD nodes currently render plain text.
  • ${name} in text interpolates a native variable — a mechanism separate from localization. Localized strings are selected per node with localization_key and come from localization_file (or an embedded locales object).
local Hud = require("demi.hud")
Hud.set_variable("game_title", "Skyforge II")
Hud.set_locale("de")             -- requires a loaded "de" entry in HUD locales
Hud.set_pseudo_locale(true)      -- pseudo-localization for layout QA

Hud.has_variable(name) returns a boolean; Hud.get_variable(name) returns the string or nil; Hud.reset_variables() clears runtime values, so unresolved names render as ${name}.

Themes

A theme document supplies styles and an optional action_map. A localization file supplies the initially loaded localization object. Hud.set_locale selects among embedded locales in the HUD; it does not load another localization file. Reference a style with "style": "menu_button". Theme styles support colors, padding, gap, font size, minimum size, and control/row height; explicitly authored text color, font size, and minimum size take precedence.

Mutating UI from Lua

Scripts change content, state, and rectangles through demi.hud:

local Hud = require("demi.hud")

Hud.set_text("title", "Ready?")           -- label/text content
print(Hud.get_text("seed_input"))         -- read a text_input

Hud.set_font("title", "asset://fonts/head")
Hud.set_font_size("title", 40)
Hud.set_color("title", 1, 1, 1, 1)
Hud.set_background_color("panel", 0.11, 0.11, 0.15, 0.8)

Hud.set_value("health_bar", 0.75)         -- progress/slider within its range
Hud.set_checked("toggle_music", true)     -- toggle state
Hud.set_disabled("start", false)          -- enable a button
Hud.set_visible("hint", false)            -- show/hide
Hud.set_opacity("fade", 0.5)              -- 0…1
Hud.set_image("portrait", "asset://ui/hero", 0, 0, 256, 256)
Hud.set_position("popup", 120, 80)        -- canvas units
Hud.set_size("popup", 480, 260)

Hud.focus_next()                          -- move keyboard/gamepad focus
local focused = Hud.focused()             -- node ID or empty string

Every setter takes a node ID string first. Hud.find(id) returns a generation-checked handle (or nil if the node is gone) for structural operations and tweens.

Runtime-created UI

local node, err = Hud.create("menu", {
    id = "continue",
    type = "button",
    text = "Continue",
    action = "confirm",
})
if not node then error(err) end
print(node.id)                  -- ID on the returned handle

local copy = Hud.clone(node, "continue_copy")  -- duplicate under a new root id
Hud.reparent(copy, "other_panel")
local kids = Hud.children("menu")   -- array of child ids
Hud.remove(node)                -- delete a runtime node

Hud.create accepts flat node fields such as id, type, text, x, y, width, and height; it does not parse nested children or layout shorthand. It returns a handle plus an error string. Keep handles for structural operations.

UI prefabs

Reusable subtrees live as *.ui.prefab.json with typed parameters (string, number, integer, boolean, array, object):

{
  "id": "confirm",
  "prefab": "ui-prefab://controls/menu_button",
  "arguments": { "label": "Confirm" },
  "overrides": {"$root": { "style": "primary" }}
}

arguments fills the prefab’s declared parameters; overrides patches fields on the expanded root node (id, children, and prefab are reserved). Expanded descendant IDs use dotted paths (confirm.hint). Unknown parameters, type mismatches, unresolved ${…} markers, missing files, reference cycles, reserved override fields, and duplicate IDs fail validation with a clear diagnostic.

Virtualization

For long inventories and chat logs, only build what is visible:

local first, count = Hud.visible_range(500, 48, 240, 480)
-- first → one-based first visible row; count → live rows needed

local layout = Hud.virtual_layout({ 48, 64, 48 })
-- layout:visible_range(scroll_offset, viewport_extent, overscan?)
-- layout:set_extent(index, extent), layout:item_offset(index),
-- layout:item_count(), layout:total_extent()

local rows = Hud.recycle_rows("inventory", template, keys, extents,
                              scroll_offset, viewport_extent)
Hud.clear_recycled_rows("inventory")  -- release the pool

visible_range(item_count, item_extent, scroll_offset, viewport_extent) covers uniform rows. virtual_layout(item_extents) returns a layout object for rows with different heights; recycle_rows(collection_id, row_template, keys, extents, scroll_offset, viewport_extent) keeps only the visible range alive against a hidden template row and returns generation-checked row bindings. Details in HUD from Lua.

Tweens and reduced motion

local fade = Hud.find("fade")
local handle = Hud.tween(fade, "opacity", 0.0, 0.35)  -- handle, property, target, seconds
Hud.tween(Hud.find("panel"), "y", 120, 0.25)
Hud.cancel_tween(handle)     -- takes the tween handle
Hud.set_reduced_motion(true) -- honor accessibility preference

Animatable properties: opacity, x, y, scale. Hud.tween returns the tween handle plus an error string; Hud.cancel_tween takes that handle.

Events

local Events = require("demi.events")

function Menu:on_start()
    self.ui_subscription = Events.subscribe("ui_event", function(e)
        if e.type == "press" and e.id == "start" then
            print("start pressed by", e.source)
        elseif e.type == "value_changed" then
            print(e.id, e.value, e.checked)
        elseif e.type == "submit" then
            print(e.id, e.text)
        end
    end)
end

function Menu:on_destroy()
    Events.unsubscribe(self.ui_subscription)
end

Event types: value_changed, focus_gained, focus_lost, submit, cancel, pointer_enter, pointer_exit, press, release, drag_start, drag, drag_end, drop, scroll.

Only the fields relevant to the type are filled — check e.type first. Events.subscribe("ui_event", …) receives the whole stream. Entity callbacks such as on_ui_submit only receive events from a HUD node whose ID matches that script's entity ID.

Accessibility

local snapshot = Hud.accessibility_snapshot()
-- array of { id, parent, role, label, description, value_text,
--            value, minimum, maximum, x, y, width, height,
--            focused, disabled, checked, focusable, offscreen }

value is numeric and value_text is its string form. Node fields: accessibility_label, accessibility_description, accessibility_hidden. Focus is shared across Tab, arrows, D-pad, and pointer; modals trap focus until closed.

Next

  • Input for virtual controls and action mapping
  • Examples tour — ui_showcase, main_menu_*