DatumHueEnterprise v0.0.24datumhue.com
Contents

Keyboard and gamepad

Your widgets work from the keyboard and a gamepad with no extra code; here is how focus moves between them, and how to follow it.

How focus moves

PressWhat it does
Tab, Shift+Tabmoves to the next or previous widget in order
An arrow key, the D-pad or the left stickmoves to the nearest widget that way, preferring one in the same row (Left, Right) or column (Up, Down)
Enter, gamepad Apresses the focused widget
Escape, gamepad Basks to go back (Handle Back)
PageUp, PageDown, Home, End, LT, RTpage through what scrolls
The right stickscrolls
The Menu key, Shift+F10, gamepad Xask for the focused element's actions (Drag and context menus)

The focus ring shows once someone uses keys or a gamepad, and not after a click or a tap.

Where focus starts

With nothing focused, the first arrow key, D-pad or stick press only shows where focus starts. It lands on the first of these that shows:

  1. the widget you gave default_focus = true;
  2. a text field made with autofocus = true;
  3. the widget at the top left.

Tab with nothing focused starts at the default_focus widget too, else at the first widget in order. An autofocus text field also takes focus as soon as it is made.

form.lualua
local ui = datumhue.ui
local token = datumhue.theme.token

local form = ui.root:panel({ width = 420, gap = 12 })
form:label({ text = "New player", font_size = "1.5rem" })
local name = form:text_input({ placeholder = "Name" })
local volume = form:slider({ value = 0.8 })
form:checkbox({ label = "Show hints" })
local row = form:panel({ flex_direction = "row", gap = 8, padding = 0 })
row:button({ text = "Cancel" })
local start = row:button({ text = "Start" })
start.default_focus = true
local hint = form:label({ color = token("text_secondary") })

datumhue.events.on("focus", function(e)
    if e.element == nil then
        hint.text = "Press an arrow key, Tab or the D-pad"
    elseif e.element == name then
        hint.text = "Type a name; Down moves on"
    elseif e.element == volume then
        hint.text = "Left and Right change the volume"
    else
        hint.text = "Enter or gamepad A presses"
    end
end)

In this example: datumhue.ui datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:label UiElement:text_input UiElement:slider UiElement:checkbox UiElement:button UiElement.default_focus datumhue.events.on UiElement.text

  • The first arrow key press lands on Start, its default_focus widget.
  • The focus event reports each change of ui.focused, so the hint follows focus without checking every frame. It also fires once right away, with nothing focused.
  • In the name field, Down moves on; on the slider, Left and Right change the volume.

Widgets that keep keys

A widget keeps the keys it uses itself, and the D-pad works some widgets the same way:

WidgetArrow keysD-pad
SliderLeft and Right change it; Up and Down move focusthe same
One-line text fieldLeft and Right move the caret; Up and Down move focusmoves focus
Multi-line text fieldmove the caretmoves the caret
List, radio groupmove the selection, except the way that would wrap itthe same
A date picker's calendarmove the daythe same
Any other widgetmove focusmoves focus

The left stick always moves focus, so it leaves every widget.

A multi-line text field made with tab_spaces keeps Tab and Shift+Tab too, as a code editor does: Tab inserts that many spaces at the caret, and Shift+Tab takes up to that many off the start of the line. Escape leaves the field.

Tab and the arrows pass over a widget that is hidden or disabled, one a popover or menu covers, and one cut off by a panel that does not scroll.

A screen reader announces a radio group by its accessible_label when focus reaches it, as it announces a button by its text. A label beside the group does not name it, so give every group a name. Assigning UiElement.accessible_label renames a group in place: focus and the selection stay where they are.

motion.lualua
local ui = datumhue.ui

local row = ui.root:panel({ flex_direction = "row", gap = 16 })
row:label({ text = "Motion" })
local motion = row:radio_group({
    options = { "On", "Calm" },
    selected = 1,
    accessible_label = "Motion",
})
print(motion.accessible_label)

In this example: datumhue.ui datumhue.ui.root UiElement:panel UiElement:label UiElement:radio_group UiElement.accessible_label

Text fields

Ctrl+Z undoes the last edit in a focused text field, and Ctrl+Shift+Z or Ctrl+Y redoes it; on a Mac, iPhone or iPad, Cmd takes the place of Ctrl. A run of typing undoes as one step. Setting UiElement.text from your code clears the field's undo history. The field then shows the new text from its start, with the caret at its end.

A multi-line text field scrolls its text under the mouse wheel and a touch drag, and passes the scroll on to the panel holding it once the text reaches its end. A mouse drag in it still selects text.

For code, make a multi-line field with syntax = "lua", line_numbers = true and tab_spaces. The text takes the theme's syntax_* colors as it is typed, a column at the field's start numbers its lines, and Enter starts each new line indented as the line above it. Tab inserts spaces at the caret. With a whole line or several lines selected, Tab indents each of them and Shift+Tab moves them back out; empty lines stay empty, and the selection stays on the same text. Ctrl+Z undoes an indent in one step. Escape leaves the field, so the keyboard can reach the rest of your app.

A single-line field takes syntax too, for a short piece of code such as the missing part of a line. It colors what is typed in the same way, while Enter still calls on_submit and Tab still moves to the next widget.

To point at lines, such as the line an error is on, set the field's line_marks. Each mark colors its line's rows and shows a sign beside the line's number, and it stays on its line as the user types above it. To bring that line into view, call reveal_line with its number: the text scrolls only when the line is out of sight, and the panels holding the field follow, as they do when Tab reaches it. The caret and focus stay where they are.

A screen reader announces a text field by its accessible_label, as it announces a button by its text. A placeholder is not announced in its place, and a label beside the field does not name it, so give every field a name. Assigning UiElement.accessible_label renames a field in place: focus, the caret and the selection stay where they are.

find.lualua
local ui = datumhue.ui

local form = ui.root:panel({ width = 320, gap = 8 })
form:label({ text = "Find" })
local find = form:text_input({
    placeholder = "A word or a phrase",
    accessible_label = "Find in notes",
})
print(find.accessible_label)

In this example: datumhue.ui datumhue.ui.root UiElement:panel UiElement:label UiElement:text_input UiElement.accessible_label

Pressing a button takes focus from a text field, and on a phone the on-screen keyboard closes with it. A button made with keep_input_focus = true leaves your app's focused field focused instead: the caret, the selection and the keyboard stay, so a code editor's Run button runs the code while the person goes on typing. While anything else has focus, pressing such a button moves focus as pressing any button does.

editor.lualua
local ui = datumhue.ui
local input = datumhue.debug.input

local box = ui.root:panel({ width = 360, gap = 8 })
local editor = box:text_input({
    value = 'print("Hello")',
    multiline = true,
    syntax = "lua",
    height = 120,
    accessible_label = "main.lua, your code",
})
local run = box:button({
    text = "Run",
    keep_input_focus = true,
    on_click = function()
        print("editor focused: " .. tostring(ui.focused == editor))
    end,
})
editor:focus()

datumhue.time.after(0.5, function()
    local center = run.bounds.center
    input.cursor(center.x, center.y)
    input.click()
end)

In this example: datumhue.ui datumhue.debug.input datumhue.ui.root UiElement:panel UiElement:text_input UiElement:button datumhue.ui.focused UiElement:focus datumhue.time.after UiElement.bounds datumhue.debug.input.cursor datumhue.debug.input.click

  • Half a second in, datumhue.debug.input presses Run, as a test would. The editor still has focus, so Run prints editor focused: true.
  • Without keep_input_focus, the press moves focus to Run, and it prints editor focused: false.

Popovers and menus

While focus is in an open popover or menu of your app, the arrows, the D-pad and the stick stay inside it. A popover made with navigation = "modal" holds them, and Tab too, for as long as it shows, whether focus is inside it or not:

  • the first press lands inside it;
  • a widget left focused behind it gets no keys;
  • PageUp, PageDown, Home, End and the triggers page what scrolls inside it.

One made with navigation = "none", such as a toast, holds nothing.

In an app running inside an element of another app's layout, Tab past a modal popover's last widget, or an arrow key that finds nothing more in it, still leaves the app, as it does past the app's own widgets.

The gamepad's buttons

  • A presses the focused widget, as Enter does, but never submits a text field: focusing the field already readies it for the platform's keyboard.
  • B asks to go back, and never reaches your widgets as a key.
  • When the D-pad, the left stick, A or a trigger acts as a key (an arrow key, Enter, PageUp or PageDown), that key reaches your widgets only: input.on_key never hears it. input.on_button and the gamepad reads see the gamepad's own buttons.

Follow focus

ToUse
ask whether an element holds focusUiElement.focused
name the element that holds itui.focused, nil while none of yours does
hear each changethe focus event of datumhue.events.on
hear focus come into or leave part of your UIUiElement:on_focus, UiElement:on_blur
move focusUiElement:focus

UiElement:on_focus and UiElement:on_blur fire for focus anywhere inside the element, and never for a move within it, so a card, a row or a toast can react to focus anywhere in it.

When focus is on a link inside a text element, ui.focused names the text element. When it is on a part of a widget, such as a number input's steppers, a picker's popover or a menu's button, ui.focused names the widget you made. That element's own focused stays false, since the link or the part holds focus, and a move between parts reports no change.

Focus and other apps

Each app keeps its focused widget while another app is in front, and gets it back when it is focused again. When the focused widget goes away while someone uses a keyboard or a gamepad, focus moves to its nearest neighbor. When one of your callbacks takes it away, that happens as the callback ends, so the callback can focus the widget it chooses first. UiElement:focus called while another app is in front, or while a dialog of the workstation's own is open, moves no keys: it picks the widget your app gets back.

Games

A game that reads the arrow keys or the D-pad itself is unaffected while it shows no widget that can take focus. One that mixes play with widgets turns focus navigation off with ui.focus_navigation = false. Its widgets then keep only their own keys, and Escape and B make no request to go back, except Escape in a focused text field.

See also

Reference used on this page: datumhue.ui datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:label UiElement:text_input UiElement:slider UiElement:checkbox UiElement:button UiElement.default_focus datumhue.events.on UiElement.text UiElement:radio_group UiElement.accessible_label datumhue.debug.input datumhue.ui.focused UiElement:focus datumhue.time.after UiElement.bounds datumhue.debug.input.cursor datumhue.debug.input.click UiElement.focused UiElement:on_focus UiElement:on_blur datumhue.input.on_key datumhue.input.on_button datumhue.ui.focus_navigation

datumhue.com

(c) 2026 Shuusei AB. Quote excerpts of this manual, with attribution to DatumHue, in your own documentation, tutorials, course materials, reviews, and other writing or video. Republishing it in whole or in substantial part needs written permission: support@datumhue.com.

Victor Mono is licensed under the SIL Open Font License 1.1
Copyright (c) 2024, Rune Bjørnerås (https://github.com/rubjo)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Try

    ↑↓ selectEnter openTab filter

    Settings

    Theme
    Text
    Print

    Keyboard shortcuts

    / or Ctrl KSearch
    ↑ ↓ EnterPick a result
    TabNext result filter
    j kNext or previous entry
    [ ]Previous or next page
    yCopy a link to the current entry
    ?These shortcuts
    EscClose

    On this page