Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

textfield

A text input: a search box, a launcher query, a password, or with multiline a message box. The engine holds what the user types (the draft); Lua sees it through callbacks and sets it only with focus_target(name):set_text. Focus, editing keys, the draft’s lifetime and password fields are on input.

A launcher: the field filters a list as the user types, the arrow keys move a selection, Enter launches.

local apps = { "Firefox", "Files", "Terminal", "Text Editor", "Settings" }
local query = state("query", "")
local selected = state("selected", 1)

local matches = query:map(function(q)
    local out = {}
    for _, name in ipairs(apps) do
        if name:lower():find((q or ""):lower(), 1, true) then out[#out + 1] = name end
    end
    return out
end)

local launcher = column { width = 320, padding = 12, spacing = 8, background = "#1E1E2E", radius = 12, children = {
    rect { width = "fill", padding = { left = 10, right = 10 }, radius = 8, background = "#313244", children = {
        textfield {
            width = "fill",
            height = 36,
            font_size = 14,
            foreground = "#CDD6F4",
            placeholder = "Search…",
            autofocus = true,
            on_change = function(text) query:set(text); selected:set(1) end,
            on_key = function(key)
                if key.name == "Down" then selected:set(math.min(#matches:get(), selected:get() + 1))
                elseif key.name == "Up" then selected:set(math.max(1, selected:get() - 1)) end
            end,
            on_submit = function() print("launch", matches:get()[selected:get()]) end,
        },
    } },
    list {
        width = "fill",
        items = matches,
        key = function(name) return name end,
        each = function(name)
            return rect {
                width = "fill", padding = { left = 10, right = 10, top = 6, bottom = 6 }, radius = 8,
                background = computed({ matches, selected }, function(found, index)
                    return found[index] == name and "#45475A" or "#00000000"
                end),
                children = { text { content = name, foreground = "#CDD6F4" } },
            }
        end,
    },
} }

return { panel { id = "launcher", layer = "top", anchor = { top = true },
    keyboard_interactivity = "on_demand", child = launcher } }

The image shows the empty search field and unfiltered list. Typing updates the list through on_change. The panel needs keyboard_interactivity for the field to get keys (panel).

Properties

textfield takes the common properties, plus:

PropertyTypeDefaultBehaviour
fontstring|BoundThe fonts chainFamily placed before the fonts chain. "" raises; an unknown family falls back to the chain
font_sizenumber|Bound, [1, 8192]12Text size in logical pixels. A textfield sizes its placeholder with it
line_heightnumber|Bound, [0.1, 10]1.2Line height as a multiple of font_size
letter_spacingnumber|Bound, [-100, 100]0Extra space between characters in logical pixels. Negative values tighten text
font_weightnumber|Bound, [1, 1000]400Font weight from 1 to 1000. A run with bold = true uses weight 700
italicboolean|BoundfalseUse the family’s italic face when available. A run with italic = true stays italic
font_variationstable<string, number>|Bound{}OpenType variation axes by 4-character tag ({ FILL = 1, GRAD = -25, opsz = 24 }), as CSS font-variation-settings. Values clamp to each face’s range; axes a face lacks are ignored. An explicit wght overrides font_weight and bold runs. Changes snap; see variable fonts
foregroundColor|Bound"#FFFFFF"A colour; a run’s color overrides it, and a textfield’s placeholder takes it unless placeholder_color is set
text_align"start"|"center"|"end"|Bound"start"Aligns lines inside the node’s own box; "start"/"end" follow each line’s reading direction. Matters only when the box is wider than the text
placeholderstring|Bound""Shown while the field is empty, focused or not. Never submitted
placeholder_colorColor|BoundforegroundColour of the placeholder
caretCaret|BoundNoneThe caret bar: { color, width, height, radius }. color defaults to foreground; width is px, default a sixteenth of font_size rounded, at least 1. height is px, or a fraction of the line height when 1 or less; default the whole line, centred on it. radius is px, default 0. Each key takes a signal. Paint only: animate snaps it
selectionSelection|BoundNoneThe selection’s ink: { background, foreground }. background is the highlight, used as given (add alpha to tint); default the field’s foreground at 30% alpha. foreground colours the selected glyphs; default the text colour. Each key takes a signal. Paint only: animate snaps it
disabledboolean|BoundfalseRenders like a field but takes no keyboard focus (Tab skips it, a press does not focus it, focus_target requests and autofocus pass over it) and draws no caret; set_text still reaches it. A focused field that becomes disabled loses focus and keeps its draft. Dim it yourself by binding colours to the same signal
max_lengthnumber|Bound0Most grapheme clusters the field holds; 0 is unlimited and a negative value is refused. Typing, paste, IME commits and focus_target(name):set_text(text) cut what they insert at the limit, secure fields included. Lowering it below the current text keeps that text; edits can then only shorten it. The cut is silent, so a limit below a password’s length truncates it
multilineboolean|BoundfalsePlain fields only: wrap the draft at the field’s width and take newlines. Return inserts one and submit_key submits; Up and Down move by visual row, Home and End to its ends, Ctrl+Home and Ctrl+End to the text’s. Paste and IME commits keep newlines, \r\n and \r as \n. The field grows from min_lines to max_lines rows; past that, or at a fixed height, its rows scroll to keep the caret in view, and the wheel scrolls them. Refused with secure_submit
min_linesnumber|Bound1Ignored without multiline: the fewest rows the field is tall; 0 is 1, a negative value is refused
max_linesnumber|Bound0Ignored without multiline: the most rows the field grows to before its rows scroll; 0 is unlimited, a negative value or one below min_lines is refused
submit_key"ctrl+return"|"return"|Bound"ctrl+return"Ignored without multiline: the chord that submits. "ctrl+return" leaves Return to insert a newline; "return" submits on Return and Shift+Return inserts the newline
initial_textstring|Bound""Plain fields only: seeds the draft once, when the field enters the tree (a new node: a changed id or key counts as new), with the value at that moment, read without subscribing: writing the signal alone does not re-resolve the field. Later changes are ignored and an emptied field stays empty; set_text pushes new text. Like set_text: cut at max_length, caret at the end, no undo history, no on_change; hidden and disabled fields are seeded too. Refused with secure_submit, control characters (but \n in a multiline field) and over 64 KiB
on_changefun(text: string)NoneFull text after every edit
on_submitfun(text: string)NoneEnter (in a multiline field, submit_key) with the full text; the field stays focused and clears. Never fires on a secure_submit field
escape"clear"|"blur"|"pass"|Bound"clear"What Escape does in a plain field. "clear" empties the draft (on_change("") if it had text), then gives up focus if on_cancel is set. "blur" keeps the draft and gives up focus. "pass" keeps both and does not take the key: it goes up through on_key, then to the surface’s on_escape. A secure_submit field ignores it and always scrubs and stays armed. Read when the field takes focus
on_cancelfun(cleared: boolean)NoneEscape; cleared says whether it removed text. A plain field clears (firing on_change("") only if there was text), gives up focus, then calls this. A secure_submit field scrubs and stays armed. Without it Escape clears and keeps focus
secure_submit{ capability: string, action: string, name?: string }|BoundNoneMakes the field masked; bytes never reach Lua. Targets: lock/authenticate, polkit/authenticate, network/connect, network/vpn_secret with a request id and key in name, secrets/store with a public name, or bluetooth/pair with a request id and MAC in name (secure fields)
mask_characterstring|Bound"•"Drawn per typed character in a secure_submit field. Only the first character counts; "" hides the length
maskedboolean|Boundtruefalse draws a secure_submit field’s typed text instead of the mask, for a prompt PAM shows (prompt.echo). Still no IME, caret or selection, and the text never reaches Lua. Turning it off scrubs what was typed under the mask. Ignored without secure_submit

The field has no intrinsic width, so give it width; without height it is one line tall (font_size times line_height), or a multiline field’s rows. It draws one line of text and a caret, vertically centred (a multiline field: rows from the top), with the same typography properties as text, the draft, placeholder and mask alike, in the fonts chain unless font names a family. Plain fields use zwp_text_input_v3 for composition when the compositor offers it and text-input enters the field’s own surface. Raw keys stay active between compositions and are suppressed during pending or active composition. Preedit text is underlined; commits and surrounding deletions call on_change. Secure fields read wl_keyboard and never send their draft to an input method.

A field with none of on_change, on_submit and secure_submit never takes focus. The draft follows the node, so give the field a stable id when siblings before it come and go (identity).

Multiline

multiline = true wraps the draft at the field’s width. Return inserts a newline and Ctrl+Return submits; submit_key = "return" swaps them, with Shift+Return inserting the newline. The field grows from min_lines to max_lines rows; past that, or at a fixed height, its rows scroll to keep the caret in view, and the wheel scrolls them. Up and Down move by visual row, Home and End to its ends, Ctrl+Home and Ctrl+End to the text’s. Up on the first row and Down on the last reach on_key. Paste, input-method commits and set_text keep newlines; a secure_submit field cannot be multiline.

local reply = column { width = 280, padding = 10, background = "#1E1E2E", radius = 12, children = {
    textfield {
        width = "fill",
        font_size = 14,
        foreground = "#CDD6F4",
        placeholder = "Write a reply. Return sends it, Shift+Return starts a new line.",
        multiline = true,
        min_lines = 3,
        max_lines = 8,
        submit_key = "return",
        on_submit = function(text) print("send", text) end,
    },
} }

return { panel { id = "reply", layer = "top", anchor = { bottom = true },
    keyboard_interactivity = "on_demand", child = reply } }

How do I…

TaskAnswer
Filter a list as the user typesThe example above: on_change sets a state, the list’s items maps it
Move a selection with the arrow keyson_key on the field, as above; pair it with scroll(name):reveal to keep the row in view (input)
Focus the field when a panel opensautofocus = true and a panel with keyboard_interactivity
Return keys to the field after a clickOne local h = focus_target("name"): focus_target = h on the field, h:request() in the on_click (input)
Close on a second Escapeon_cancel(cleared): close only when cleared is false
Ask for a passwordsecure_submit = { capability = "lock", action = "authenticate" } (secure fields)
Submit a password from a buttonsubmits_secure = true on the clickable node
Style the box around the fieldWrap it in a rect with background, radius and border_*; the field draws only text and caret
Debounce a searchsignals

Gotchas

TrapFix
The field does not appearIt has no intrinsic width. Give it width
Typing does nothingThe surface needs keyboard focus (keyboard_interactivity on a panel), and the field needs on_change, on_submit or secure_submit
on_cancel alone never firesIt does not make the field focusable. Add on_change or on_submit
You cannot read the draft from LuaIt arrives only through on_change and on_submit. focus_target(name):set_text writes it; Enter and Escape clear it; removing the node drops it
on_submit never fires on a password fieldA secure_submit field sends to its capability instead
font on a textfield is refusedFields use the fonts chain
background on a textfield is refusedIt is not a box. Wrap it in a rect

See also: input, list, text, surfaces: panel.

Source: vocabulary, content parsers, secure_submit, plain fields, focus.