A node in the UI tree, returned by the constructor methods on a parent element (ui.root:panel{...}, panel:button{...}, ...) and by source:mount(opts) on a render target. Methods adjust layout, content, hit-testing, and appearance.
local panel = datumhue.ui.root:panel({
flex_direction = "column",
padding = 16,
gap = 8,
})
Fields 33
Field
Type
Access
Description
visible
boolean
read/write
Whether the element and its children are shown. On a touch control, the value you set: a touch_only control is also hidden while input.last_source is not touch.
Layout display mode. Assigning none removes the element from layout while keeping its subtree (and scroll position) alive; other values restore it. A text input lays its text and children out in a flex row, and assigning it a block or grid display raises.
The element's laid-out border box in UI px, relative to the top-left of the calling app's content area (the window for the root app); scrolling a container above it moves it. Read-only. nil before the element's first layout and while it or an ancestor has display = "none". Layout runs after the app's frame, so a change made now reads back from the next frame.
Background color. Assign a datumhue.theme.token(...) to follow the theme, or a Color to pin it; reading gives the resolved Color shown. A button keeps an assigned fill through hover and press, which draw its label color (on a card button, the theme's button text color) over it at 8% and 10%, and reading it then gives that layered fill; while disabled it shows the theme's disabled fill. A button's border takes its fill's color too, unless the button was given a border_color.
text
string?
read/write
Text content of a label, text input, button or link, and the source a code element shows; nil on other elements, including card buttons. A label with max_lines reads its whole text, and one with marquee on the text it loops. Assigning relabels a button or link in place, keeping its focus, hover and pressed state, and stops following any locale; assigning to a text input replaces its value silently (no on_change); assigning to a code element shows the new source, colored as before. Raises on a card button.
The name assistive technology announces for a button, image, text input or radio group, in place of a button's text. Reads the resolved name, or nil when none was set; nil on other elements. A message follows the active locale. Assigning keeps focus and state: a text input's text and selection, and a radio group's selection; assigning nil clears the name, so a text button is named by its text again. Raises on other elements, when nil is assigned to a card button, which needs a name, and when a name is assigned to an image mounted with decorative = true, which reads nil.
marquee
boolean
read/write
Whether an overflowing label loops its text horizontally, pausing briefly at each wrap. Text that fits stays still. While on, the label lays its text out on one line, breaking no word: as wide as that line where there is room, and in a narrower row or column as narrow as the room it gets, with no smallest width, looping what does not fit. The looping line is clipped at the label's sides. Writable on labels; false elsewhere, and raises on assignment to a non-label, and on turning it on for a label with max_lines.
An image's tint: a color each texel is multiplied by, alpha included, so white leaves the image as it is and a lower alpha fades it. Assign a datumhue.theme.token(...) to follow the theme, as color does, or a Color to pin it; nil takes it off. Reads the color shown in the current theme, or nil when the image has none. nil on other elements; assigning to one raises.
max_lines
integer?
read/write
The most lines a label shows; nil for no limit. A longer text ends in "..." on the last line, cut at a glyph. While clamped, a word wider than the label breaks at a glyph, so a single long word is cut too and the label can shrink to one glyph wide in a row; nil puts the label's own overflow_wrap back. A clamped label narrower than "..." shows as much of it as fits, clipped at its box. The label is as wide as its whole text would be and as tall as the lines it shows, and the cut follows its width; text still reads the whole text. nil on other elements. Assigning raises below 1, on a label with marquee on, and on any other element.
How a label breaks a word wider than its box; "break_word" by default. Either way the label's smallest width in a row is its widest word, so it is narrower than that word only when its box is: given a width or a max_width such as "100%", or stretched across a narrower column. While max_lines is set the clamp breaks such a word at a glyph, and a label with marquee on keeps its text on one line; the label breaks as this says again once they are off. nil on other elements. Assigning raises on other elements, and on nil.
placeholder
string?
read/write
A text input's placeholder, shown while it is empty; nil when it has none. Assigning replaces it in place, keeping focus and the typed value, and nil removes it. nil on other elements; assigning to one raises.
A text input's leading icon: the image drawn inside its border at its start, one text line tall and level with its first line, as a mask in the theme's secondary text color (its alpha is the shape; its own colors are not drawn). The field's start padding makes room for it, so the text and the placeholder start after it, and the field grows no taller. A press on it focuses the field with the caret at the end; it is no focus stop and has no accessible name. Assigning replaces it and nil takes it off; removing the image takes it off too. nil on other elements. Assigning raises on other elements, and an image on a field with line numbers or whose start padding is a percentage.
clearable
boolean
read/write
Whether a text input shows a clear button at its end while it holds text and is enabled. A press on it empties the field as deleting the text would, so on_change fires with "", and leaves focus in the field; it is no focus stop, and its accessible name is the datumhue-text-input-clear message. While it shows, the field's end padding makes room for it: at least 24 UI px wide (44 at touch density), as tall as the field inside its border, which grows no taller. false on other elements. Assigning raises on other elements, and true on a multi-line input or one whose end padding is a percentage.
Maximum scroll offset (content minus viewport, UI px; zero until first layout). nil for non-scroll elements.
enabled
boolean
read/write
Whether an interactive widget (a button, checkbox) accepts input. Setting false grays it out and suppresses its click / keyboard activation; true restores it. Reads true on non-interactive elements.
checked
boolean
read/write
Whether a checkbox is checked. Assigning updates it silently (the on_change callback fires only on user toggles, not programmatic writes). Reads false on non-checkbox elements; assigning to one raises.
A value-bearing widget's current value: a number for a slider or number input, an integer instant (Unix microseconds) for a date/time picker, or a Color for a color picker. Assigning updates it silently (no on_change). nil on elements that hold no value; assigning to one raises.
Example
local volume = panel:slider({ min = 0, max = 10, step = 1 })
volume.value = 7print(volume.value) --> 7.0local tint = panel:color_picker({})
tint.value = datumhue.color("#e67e80")
local due = panel:date_picker({})
due.value = datumhue.time.from_calendar({ year = 2026, month = 10 })
print(due.value) --> 1790812800000000
min
number?
read/write
A slider's range minimum. Assigning re-clamps value into the new range. nil on non-slider elements; assigning to one raises.
max
number?
read/write
A slider's range maximum. Assigning re-clamps value into the new range. nil on non-slider elements; assigning to one raises.
step
number?
read/write
A slider's keyboard / track-click step increment. nil on non-slider elements; assigning to one raises.
selected
integer | integer[] | nil
read/write
Current selection as a 1-based index. A radio group or single-select list or table reads an integer (or nil); a multi-select list or table reads an index array. A data table's indices are display positions, and its selection follows the rows across a sort. Assigning selects silently (no on_change): an integer (or nil to clear) for radio/single, an array for multi. nil on elements that aren't a radio group, list, or table; an out-of-range index, or an array on a radio group, raises.
Whether a pointer (the mouse or a touch) holds a press on the element; on a touch control, whether a touch holds it. Read-only; false on non-interactive elements.
hovered
boolean
read-only
Whether a pointer (the mouse or a touch) is over the element while none holds a press on it. Read-only; false while it is pressed and on non-interactive elements.
focused
boolean
read-only
Whether this element itself holds input focus, so keys and pad activation go to it. false while its app is not the input owner (another app is focused, or a workstation dialog is open); the element the app last focused gets focus back when the app is focused again. Read-only.
default_focus
boolean
read/write
Whether an arrow key, D-pad or left stick press with nothing focused in this element's navigation scope (the app, or the popover or menu it is in) lands here, while it shows and is enabled: ahead of an autofocus text input and of the top-left widget. Tab and Shift+Tab with nothing focused start here too, ahead of the first (last) widget in tab order. Where several have it, the last one set wins. Default false. It never takes focus by itself. Reads false on an element that cannot take focus; assigning to one raises.
strikethrough
boolean
read/write
Whether a line is drawn through a text label. Assigning toggles it. Reads false on non-text elements; assigning to one raises.
underline
boolean
read/write
Whether a line is drawn under a text label. Assigning toggles it. Reads false on non-text elements; assigning to one raises.
A touch stick's deflection: y up, length at most 1, zero at rest. Read-only. nil on other elements.
deflection
number?
read-only
A touch stick's distance from the base's center, in units of radius. It is not clamped, so a value past 1 means the thumb is beyond the travel; 0 at rest. Read-only. nil on other elements.
Marks on lines of a multi-line text input or a code element. Each draws a band in its kind's color behind every row of its 1-based line, wrapped rows included, under the text and scrolling with it. With line_numbers, the sign cell after the numbers shows its sign in its kind's color, so a mark never rests on color alone; without them a mark is the band only. Neither moves the text, and marks are not announced: say the line in words too. Reading gives a new array in line order, {} when there are none and on any other element. Assigning replaces every mark, and {} clears them. On a text input a mark keeps to its line as the user types, pastes, cuts, undoes and redoes: a mark below the edit moves by the lines it adds or removes, and a mark on a line it changes or removes goes. Assigning text clears them. Assigning raises, changing nothing, on a single-line text input or any other element, for a line below 1 or past the last, and for two marks on one line.
Example
local answer = panel:code({
text = "local total = 0\nfor i = 1, 3 do\n total = total + i\nend",
syntax = "lua",
line_numbers = true,
width = 320,
})
answer.line_marks = {
{ line = 3, kind = "error" },
{ line = 1, kind = "info" },
}
print(answer.line_marks[1].line, answer.line_marks[1].kind) --> 1 info
Methods 54
UiElement:remove
method
UiElement:remove()
Remove the element and its children. Raises on ui.root (the app's own content area) — remove its children or individual elements instead.
Move the listed children of this element to the front, in the given order; the other children follow in their current order. The elements are kept, with their focus, hover, scroll, handlers and state; layout, navigation by the arrow keys and the Tab order follow from the next frame. A call that leaves the order as it is changes nothing. Raises when an entry is not a child of this element or repeats an earlier entry, before moving any.
Example
local first = panel:label({ text = "First" })
local second = panel:label({ text = "Second" })
panel:reorder({ second, first })
Create an in-app navigation region under this element and return its Router. The region fills this element; declare routes with router:route and navigate with show/push/pop/go. Pages are retained (hidden) when backgrounded, so going back restores their scroll and state.
Go back a page, as router:pop() does, on each cancel request (Escape, gamepad B, Back) that reaches the router while it has a page above its root; when false, the router never takes one.
Set, replace, or clear (pass nil) an open_button's pick handler at runtime — the path to bind a <open_button> declared in DHML from the hosting app. Called with the picked File / File[] / Dir on success (never on cancel); with none bound, a click opens no dialog.
Example
local open = panel:open_button({ text = "Open", mode = "files" })
open:on_pick(function(files)
for _, file inipairs(files) doprint(file.name)
endend)
UiElement:on_save
method
UiElement:on_save(callback?: function)
Set, replace, or clear (pass nil) a save_button's save handler at runtime. Called with the read-only File handle after the bytes are committed (never on cancel).
Example
local export = panel:save_button({ text = "Export", source = "1,2\n" })
export:on_save(function(file)
print("saved " .. file.name)
end)
Set the bytes a save_button writes at its next click, captured now. Pass nil to clear (a click then does nothing). The way to keep a save button's payload current — call it whenever the app's data changes. Raises on a non-widget element.
Set the text a copy_button places on the clipboard at its next click, captured now — a string or UTF-8 Bytes (non-UTF-8 raises). Pass nil to clear (a click then does nothing). The way to keep a copy button's payload current — call it whenever the app's data changes. Raises on a non-widget element.
The i-th row (1-based, display order — the sorted order while sorted) of a data table as a {column = value} table over its configured columns, holding the data's own values rather than the text the cells show; a cell missing from the data reads nil. Raises on a non-table element or an out-of-range index.
Example
local scores = datumhue.bytes("name,score\nAda,3\nLin,5\n"):csv()
local tbl = panel:table({
data = scores,
columns = { { column = "name" }, { column = "score" } },
})
local row = tbl:row(2)
print(row.name, row.score) --> Lin 5
UiElement:on_hover
method
UiElement:on_hover(callback?: function)
Set, replace, or clear (pass nil) the hover-enter handler at runtime. Fires when a pointer (the mouse or a touch) enters this element or any of its descendants.
Set, replace, or clear (pass nil) the hover-exit handler at runtime. Fires when the last pointer leaves this element and all of its descendants; a press held inside it keeps it entered until the press ends.
Set, replace, or clear (pass nil) the press handler (fires while held) at runtime. Fires only on an interactive element. Raises on a card button's content, whose presses are the card's.
Example
local key = panel:button({ text = "Hold" })
key:on_press(function()
key.text = "Held"end)
UiElement:on_release
method
UiElement:on_release(callback?: function)
Set, replace, or clear (pass nil) the release handler at runtime. Fires only on an interactive element. Raises on a card button's content, whose presses are the card's.
Example
local fire = panel:button({ text = "Fire" })
fire:on_release(function()
print("released")
end)
UiElement:on_double_click
method
UiElement:on_double_click(callback?: function)
Set, replace, or clear (pass nil) the double-click handler at runtime. Fires on a second press within the double-click window. Fires only on an interactive element (a panel or button). Raises on a card button's content, whose presses are the card's.
Set, replace, or clear (pass nil) the focus handler. It fires when keyboard focus enters this element's subtree: the element, or an element inside it (a popover or menu hung from it or from an element inside it included), gains focus while focus is outside it. A move between two elements inside the subtree fires nothing, as with on_hover, and focus on a part of a composite widget is focus on the widget (see ui.focused), so a move between its parts fires nothing anywhere. It fires on any element, focusable or not, whatever moved focus: a press, Tab, focus navigation, focus(), focus moving off a removed or hidden element, a closing popover giving focus back, or your app losing focus and getting it back. Changes within one frame count as one move, to where focus ended up. For each move your on_blur handlers run before your on_focus handlers, each innermost element first. An on_blur follows once focus leaves, unless the element is removed first. Setting a handler on another app's element raises.
Set, replace, or clear (pass nil) the blur handler. It fires when keyboard focus leaves this element's subtree, for an element outside it or for none, and never for a move between elements inside it. It pairs with on_focus and follows its rules for what is inside the subtree, which focus changes count and the order handlers run in. An element removed while focus is inside it hears no on_blur: its handlers go with it. Setting a handler on another app's element raises.
Set, replace, or clear (pass nil) the element's cancel handler. While one is set, the element consumes every cancel request (Escape, gamepad B, Back) that reaches it: one made while focus is on it or inside it, or on datumhue.ui.root one made with nothing focused. Setting a handler on another app's element raises; a window manager sets one on the frame wm.wrap_app returned to take the requests the wrapped app leaves, and those any app living in its window leaves.
Set, replace, or clear (pass nil) the resize handler. It fires once after the element's first layout following the call (in the next frame when it is already laid out), then whenever either side of its laid-out border box changes by 0.5 UI px or more: at most once per frame, with the latest size, one frame after the layout, never for a move alone. size is the border-box size in UI px, nil when the element leaves layout (it or an ancestor gets display = "none"), and the size again when it returns, so it always agrees with bounds. On datumhue.ui.root it fires on window resize, Display size (zoom) change, window-manager bounds or inset changes, and snaps. Setting a handler on another app's element raises.
Set, replace, or clear (pass nil) the handler for changes to this element's own visible (a parent's does not count). It runs in the frame after the change, whatever made it: your own write, a light dismiss or cancel request closing a popover, or the workstation showing a window manager's frame when the framed app's init ends. Changes within one frame coalesce, and it runs only with a value that differs from the one it last reported, or from the element's value when the handler was first set; replacing the handler keeps that value. Setting a handler on another app's element raises.
Set, replace, or clear (pass nil) the drag handler, for the mouse and touch alike. A primary press (the left mouse button or a touch contact) on the element or a descendant, including a button or checkbox inside it, arms a drag; the nearest element with a handler takes it once the pointer has moved more than 4 UI px (mouse) or 8 (touch) along either axis. A press on a slider, scrollbar, text input or color picker inside the element keeps its own drag, and a press in a popover or menu anchored inside it is the overlay's. For a touch, a scroll container inside the element takes the movement instead when it runs along an axis the container scrolls. From its start the drag owns the pointer: nothing under it clicks or activates, and it follows the pointer outside the element and the window until the release, a touch cancel, window focus loss, a workstation dialog opening over it, or the element being removed or hidden (visible = false, or display = "none" on it or an ancestor). Only the first touch contact drives a drag, and keyboard focus plays no part: a press that moves focus to another app keeps its drag. A replaced handler hears the rest of the drag; clearing the handler cancels it. Setting a handler on another app's element raises, and so does setting one on a card button's content, whose presses are the card's.
Set, replace, or clear (pass nil) the context handler: the element's actions are asked for, as the right-click menu or an item's Actions. It fires on a right-click (the right mouse button released over the element), a touch held 500 ms within 8 UI px of where it landed (that touch then clicks nothing, and neither scrolls nor drags), the Menu key or Shift+F10 while the element has keyboard focus, and gamepad X (the west face button) while it has focus and ui.focus_navigation acts. A request goes to the nearest element with a handler, walking up from the element under the pointer or the focused one within its app, and only that handler hears it; a request in a popover or menu popup anchored inside the element is the overlay's. A touch on a slider, scrollbar, text input or color picker keeps its press, so it makes no request. Input reads are unchanged: the key and the buttons still read as pressed. Setting a handler on another app's element raises, and so does setting one on a card button's content, whose presses and focus are the card's.
Move keyboard focus to this element. Takes effect on focusable widgets (a text input, button, checkbox, slider, radio group, or list); a no-op on elements that cannot hold focus. The focus ring shows when the last input came from the keyboard or a gamepad. Called while its app is not the input owner (another app is focused, or a workstation dialog is open), it moves no keys: the element gets focus when its app is the input owner again.
Set the color of a label's or link's text, or of a button's label. Pass a datumhue.theme.token(...) to follow the theme; omit / pass nil to re-follow the default text color. A disabled button shows the theme's disabled label color, and this one again once enabled.
Batch-update layout / style plus source, color, text, visible, or material. The sizes, padding, border width, corner radius, gaps, colors and fonts it writes stay as written through later theme and density changes. On a button, text and the font keys style its label. On a text input, the room its leading_icon, line numbers and clear button take adds to the padding it writes, so a percentage on a side one of them takes raises. A column flex_direction, a wrapping flex_wrap or a block or grid display raises too.
Example
local status = panel:label({ text = "Draft" })
status:update({
text = "Saved",
font_weight = "bold",
padding = 4,
color = datumhue.theme.token("success_background"),
})
optsui.ElemUpdateOptionsoptions43 keys, 34 inheritedOmitted keys keep their current value.
New text — a plain string (stops following any locale), or a datumhue.i18n.t(...) message (follows the active locale). Relabels a button or link in place; raises on a card button. On a code element a string replaces its source, as UiElement.text does, and a message raises.
font_size
number | string
New label font size: a number (pixels) or a unit string. Pins the size (stops following the theme).
Cursor position in UI px from the top-left of the element's border box (bounds.min), at every zoom and stretch mode; negative or past bounds.size off the element. Nil off the window and while the element has no bounds.
Scale an element-relative position in UI px (what mouse_position returns) to the source's own pixels, or nil: an image's pixels, or a draw canvas's or scene's width and height, whatever resolution it renders at. Scene:screen_to_ray takes these pixels.
Create a child interactive button. Without text the button is a card: the elements created under it form its content, the whole card is one focus stop and one pointer target (a press on any of its content activates it), and creating an interactive widget inside it raises, a table and a book page with links, widgets or chapter navigation included, as does a press or drag handler on its content. A button with text has a border of the theme's border_width unless border is set, outside its padding, so a button sized by its content is that much larger on each side; a card has a border only when border is set. A button that takes the theme's fills draws its border in the theme's button_border, which stands out from the surface under it. A disabled button, a button given a fill and a button in a menu draw their border in their fill.
Example
panel:button({
text = "Save",
on_click = function()
print("saved")
end,
})
local card = panel:button({ accessible_label = "Open notes" })
card:label({ text = "Notes" })
The name assistive technology announces, in place of the label's text. Required without text. A datumhue.i18n.t(...) message follows the active locale.
focusable
boolean
true
Whether the button takes focus: a navigation stop with a focus ring. false leaves it activated by pointer and touch only; it keeps its accessible name.
enabled
boolean
true
Whether the button starts enabled. A disabled button is grayed out and ignores clicks and keyboard activation until elem.enabled = true.
keep_input_focus
boolean
false
Whether pressing the button leaves a focused text input of your app focused, keeping its caret, its selection and a phone's on-screen keyboard. Otherwise the press takes focus from the input, as a press anywhere outside it does.
on_click
function
–no default
Click callback.
on_hover
function
–no default
Called when a pointer (the mouse or a touch) enters the button or any of its descendants.
on_hover_exit
function
–no default
Called when the last pointer leaves the button and all of its descendants; a press held inside it keeps it entered until the press ends.
on_press
function
–no default
Called when a pointer (the mouse or a touch) presses the button with its primary button.
on_release
function
–no default
Called when that press ends: released anywhere, or taken by a drag, a drag scroll or a long press.
on_double_click
function
–no default
Called on a double-click: two presses on the button within the double-click window.
Create a child button that opens a file dialog when clicked, and calls on_pick with what the person picks. Only the click opens the dialog, so your app cannot open one by itself. In a browser that does not let a web page change the person's files, a picked folder is read-only.
Seeds the dialog's starting location. Best-effort: native dialogs open there; browser pickers ignore it.
title
string
the dialog's own title
Dialog title.
on_pick
function
–no default
Called on a successful pick with the picked handle (never on cancel). Optional; bind later with elem:on_pick (e.g. from a hosting app via book:element). With no handler bound a click opens no dialog.
enabled
boolean
true
Whether the button starts enabled. Setting false grays it out and ignores clicks.
Create a child button that opens a save dialog when clicked and atomically writes source to the chosen file. The click is the user gesture. On success on_save fires with the read-only File handle; cancel fires nothing. With no source set the click does nothing.
Create a child button that places its source text on the system clipboard when clicked. The click is the user gesture — there is no programmatic copy, so an app can neither read nor spam the clipboard on its own. On success on_copy fires; a failed write logs. With no source set the click does nothing.
The text a click places on the clipboard, captured now. A string or UTF-8 Bytes (non-UTF-8 raises — the clipboard is text). Bind it later with elem:on_copy_source instead to keep the payload current.
on_copy
function
–no default
Called after a click places the text on the clipboard. In the browser the write is asynchronous — it fires when the write is handed to the clipboard, and a rejected write logs instead.
Create a child link styled as link text. Clicking it opens url in a new browser tab; opening only ever happens from this user click, so an app cannot open URLs on its own. url must be http, https, or mailto (any other scheme raises). Hovering shows the destination host.
Example
panel:link({
text = "Lua manual",
url = "https://www.lua.org/manual/5.5/",
})
Link text (required) — a plain string, or a datumhue.i18n.t(...) message that follows the active locale.
url required
string
–no default
Destination URL (required). Must be http, https, or mailto; any other scheme raises. Clicking opens it in a new browser tab — there is no programmatic open, so a user gesture is always required.
Create a date picker: a field that opens a calendar popover. Selecting a day fires on_change with the selected instant (Unix microseconds); read/write it via value (nil until picked). Build / decode the value with time.from_calendar / time.calendar.
Create a time picker: a field that opens a popover with 24-hour HH:MM steppers. Editing the time fires on_change with the selected instant (Unix microseconds).
Create a combined date+time picker: a calendar popover with 24-hour HH:MM steppers. Picking a day or editing the time fires on_change with the selected instant (Unix microseconds).
Create a color picker: a field (a swatch + the hex string) that opens a popover with a saturation/value square, a hue strip, a live preview, a hex field, and optional preset swatches. Dragging, committing the hex, or clicking a preset fires on_change with the picked color (sRGB). Read or write the current color via the value property; a programmatic write is silent.
Preset swatches shown as a clickable row (colors or theme tokens). Clicking one sets the color and keeps the popover open.
enabled
boolean
true
Whether it starts enabled.
open
boolean
false
Start with the popover open.
on_change
function
–no default
Called with the picked Color when the user drags the square/strips, commits the hex field, or clicks a preset. A programmatic value write does not fire it.
The name assistive technology announces for the group; a label beside it does not name it. A datumhue.i18n.t(...) message follows the active locale. Also a read/write property.
enabled
boolean
true
Whether it starts enabled. A disabled group ignores clicks and keyboard navigation until elem.enabled = true.
on_change
function
–no default
Called with the selected 1-based index (integer) when the user picks an option. A programmatic elem.selected = ... write does not fire it.
Row labels (required); one selectable row per entry, in order.
selected
integer[]
no row is selected
Initially-selected 1-based indices. A single-element array selects one row; several pre-select multiple (multi-select only).
multi
boolean
false
Allow selecting multiple rows at once.
enabled
boolean
true
Whether it starts enabled. A disabled list ignores clicks and keyboard navigation until elem.enabled = true.
on_change
function
–no default
Called on user selection with the new selected value — a 1-based integer for a single-select list, a 1-based index array for multi. A programmatic elem.selected = ... write does not fire it.
Create a child data table bound to a DataHandle: a header row over a scrolling body that reads only the rows in view, copying no row into Lua. State is the selected property (a 1-based display index, or an index array when multi); on_change fires on user selection. tbl:row(i) reads one row on demand.
The tabular data to display (required). Only the rows in the visible scroll window render, and no row is copied into Lua; tbl:row(i) reads a single row on demand.
Column specs (required, in display order). Raises when a named column is missing from already-loaded data.
sortable
boolean
false
Make the headers clickable: a click sorts by that column (stable), a second click on the same header reverses. The selection follows the rows across a sort.
multi
boolean
false
Allow selecting multiple rows at once.
on_change
function
–no default
Called on user row selection with the new selected value — a 1-based display index for a single-select table, an index array when multi. A programmatic elem.selected = ... write does not fire it.
Create a child text input. Its content is the text property; on_change fires on each user edit and on_submit on Enter (single-line). Its children share the row inside its padding with its text, after it unless reorder lists them first, and its layout keys act on that row. A column flex_direction, a wrapping flex_wrap or a block or grid display raises, here and in update, as does assigning a block or grid display. In a row aligning its items at their baselines, a single-line text input aligns at the bottom of its line of text, where its caret ends. A multi-line one aligns at the bottom of the area inside its padding. A child laid out in the row with its text can change this; a popover, a child whose display is none and one given left, top, right or bottom in its options or update cannot. The first such child aligned at its baseline (by its align_self or the input's align_items) gives the input its baseline, or, with none, the first such child reorder lists before the text. An auto top or bottom margin keeps a child from aligning at its baseline.
The name assistive technology announces for the input; its placeholder is not announced in its place. A datumhue.i18n.t(...) message follows the active locale. Also a read/write property.
An image drawn inside the field's border at its start, one text line tall, as a mask in the theme's secondary text color; a press on it focuses the field. Also a read/write property.
clearable
boolean
false
Show a clear button at the field's end while it holds text. Raises on a multi-line input. Also a read/write property.
max_length
integer
no length limit
Maximum number of characters the field accepts; a negative number sets no limit.
multiline
boolean
false
Allow multiple lines: Enter inserts a newline instead of submitting.
tab_spaces
integer
Tab moves focus, as on every other input
On a multi-line input, Tab types this many spaces at the caret, or over a selection of part of a line. With a whole line or several lines selected, it adds them to the start of each of those lines that is not empty. Shift+Tab takes up to as many off the start of each line the selection or the caret is on. Escape leaves the input. Raises on a single-line input, or for a number below 1 or above 16.
Colors the text as source code of this language as it is typed, in the theme's syntax_* colors. In a multi-line input, Enter starts the new line indented as the line above it; a single-line input still calls on_submit on Enter. A disabled input shows its text in one color.
line_numbers
boolean
false
Numbers the lines in a column at the input's start that scrolls with the text; a wrapped line is numbered on its first row. After the numbers the column keeps room for one more character, where line_marks show their signs. Raises on a single-line input, with leading_icon, and when padding gives the start a percentage.
ligatures
boolean
true
Whether the font may join signs such as >= or -> into one shape. False draws each character as typed, in the text, the placeholder, the line numbers and any element inside the input.
filter
string
every character can be typed
A Lua character-class pattern (e.g. "%d", "[%d.-]") restricting which characters can be typed or pasted, matched per character. It masks the character *set*, not structure: validate the whole value in on_change or on_submit (and prefer number_input for numbers). ASCII-oriented like Lua's own %-classes; raises if the pattern is malformed.
What the field holds, which picks a browser's on-screen keyboard and the label of its Enter key. A filter that takes only digits, or digits and both decimal separators, wins: the field keeps its digit keypad and the purpose changes nothing. It never limits what can be typed; outside a browser it changes nothing.
select_all_on_focus
boolean
false
Select all the text when the field gains keyboard focus.
enabled
boolean
true
Whether it starts enabled. A disabled input ignores keyboard and pointer input until elem.enabled = true.
on_change
function
–no default
Called with the new text (string) on each user edit. A programmatic elem.text = ... write does not fire it.
on_submit
function
–no default
Called with the text (string) when Enter is pressed in a single-line input.
autofocus
boolean
false
Take keyboard focus as soon as the input is created, as focus() does: created while its app is not the input owner, it moves no keys and is the focus the app gets back once it is. If several inputs request it in one frame, the last one created wins.
Called when a cancel request reaches the focused input: Escape (also while datumhue.ui.focus_navigation is off), gamepad B or Back. The input then loses focus, so the next request goes on past it.
Create an overlay popover anchored to this element, positioned on side/align with gap, auto-flipped to stay on-screen. Fill it with child widgets; show/hide via the visible property.
Example
local help = panel:button({ text = "Help" })
local tip = help:popover({
side = "right",
gap = 8,
padding = 8,
visible = true,
light_dismiss = true,
})
tip:label({ text = "Drag to pan, scroll to zoom." })
Whether it starts shown; toggle later via the visible property.
light_dismiss
boolean
false
Close the popover on a pointer press outside it. Off: fully app-controlled via the visible property. On: light-dismisses like a menu (pressing the anchor still toggles it).
Create a dropdown menu anchored to this element: a label trigger button and a popup you fill with child widgets (each item carries its own callback). Clicking the button toggles the popup; clicking outside or pressing Escape closes it; activating a button item runs it and closes the menu.
Example
local file = panel:menu({ label = "File" })
file:button({
text = "New",
on_click = function()
print("new")
end,
})
file:button({ text = "Quit", on_click = datumhue.app.terminate })
panel:label({
text = "Chapter one",
font_size = "1.5rem",
font_weight = "bold",
})
panel:label({
text = "A long description of the chapter.",
max_lines = 2,
})
Whether the font may join signs such as >= or -> into one shape. False draws each character as typed, in the text and in any element inside the label.
max_lines
integer
no line limit
The most lines the label shows, at least 1. A longer text ends in "..." on the last line, cut at a glyph; a word wider than the label breaks at a glyph. Also a read/write property.
Create a child code element: source code drawn as a book page draws a code listing, on the theme's surface_sunken fill in its normal text size, following theme and density switches. It wraps at its width, between two characters where a run with no space is wider than it. It takes no focus, keys or presses, so a press on it reaches the element holding it, and it may go inside a card button. Assistive technology reads it as code, with its source as the value.
Example
panel:code({
text = 'local name = "Hue"\nprint("Hello, " .. name)',
syntax = "lua",
line_numbers = true,
width = 320,
})
Colors the source as code of this language, in the theme's syntax_* colors.
line_numbers
boolean
false
Numbers the lines in a column at the element's start, in the theme's secondary text color; a wrapped line is numbered on its first row. After the numbers the column keeps room for one more character, where line_marks show their signs.
ligatures
boolean
true
Whether the font may join signs such as >= or -> into one shape. False draws each character as typed, in the source, its line numbers and any element inside it.
Create a child touch stick: a thumb stick a touch drives. The element's box is the area a touch can start it in; an axis the style leaves unsized is 2 * radius UI px. A touch that starts on it is the stick's until it ends: input.touches() and the elements under it never see it. While it holds the stick, stick, deflection and pressed follow it, and the stick adds to your app's input.action_vector past a 0.25 dead zone. A mouse click never moves it. Raises inside a card button.
Example
-- A row for the controls, the stick on its left.local controls = panel:panel({
flex_direction = "row",
width = 320,
height = 160,
padding = 16,
})
local move = controls:touch_stick({ width = 160, radius = 48 })
-- At rest until a touch starts on it; y is up.print(move.stick, move.deflection) --> vec2(0.0, 0.0) 0.0
Create a child touch button that holds a named action down while a touch holds it: from the touch's start until it ends, wherever the finger moves, input.is_action_down reads true, and is_action_pressed and is_action_released each read true in one tick, whether or not bind_action bound the name. A touch that starts on it is the button's until it ends; the mouse and keys never press it. Removing, hiding or disabling a held button releases it. Raises inside a card button.
Scroll a multi-line text input so its 1-based line shows. While the line's first row is not wholly in view, the text scrolls to put that row in the middle, or as near as the text's ends allow; a line already in view leaves the text where it is. Then the scroll containers holding the input move, innermost first, as for an element Tab focuses, so the line shows in the window and above an on-screen keyboard. The caret, the selection and focus stay where they are, no keyboard opens and on_change does not fire. Called before the input is first laid out, it takes effect once it is. Raises on a single-line text input or any other element, and for a line below 1 or past the last.
Example
local lines = {}
for n = 1, 40do
lines[n] = "print(" .. n .. ")"endlocal editor = panel:text_input({
multiline = true,
line_numbers = true,
value = table.concat(lines, "\n"),
width = 320,
height = 120,
})
editor:reveal_line(30)