Skip to content

Guide

State and input #

Immediate mode does not mean that there is no state. This page tells you which state you own, which state Knots owns, and how Knots finds it each frame.

Two kinds of state #

State Owner Examples
Application data You The todo list, the text of a draft, a slider value, the selected tab.
Widget state Knots Focus, hover, scroll offset, caret position, text selection, open menus, animations.

Components get your data through pointers, for example Checkbox.checked: *bool and TextInput.buf. The component changes the value, and you read the value in the same frame.

Keys #

Each component has a key: ui.Key. Knots uses the key to find the widget state of the component from the last frame. A key must be unique in the frame and must be the same in each frame.

Constructor Use
.src(@src()) A key from the source location. Use it for a component that has one instance.
.str("name") A key from a string. Use it when other code must know the key.
key.indexed(n) A new key from a key and a number. Use it in loops and in custom components. You can call indexed more than one time.

Loops need indexed keys. In a loop, .src(@src()) gives the same key for each item. Use a stable ID from your data, for example ui.Key.str("row").indexed(item.id). Do not use the list position if items can move or be removed.

key.hash() returns the element ID. Use it with functions that take an ID, for example frame.ui().focused(id).

Widget state lifetime #

Knots keeps widget state after a component stops being emitted. It removes the state after 1800 frames without use. Thus a hidden tab keeps its scroll position and text selection when it shows again.

To change this time, set state_ttls in ui.UI.Config. Each kind of state has its own field, for example scroll and text_input.

Frame memory #

frame.arena() returns an allocator that Knots resets after each frame. Use it for strings and slices that you make for one frame:

const label = try std.fmt.allocPrint(frame.arena(), "{d} items", .{count});
try frame.e(Text{ .key = .src(@src()), .content = label });

Do not keep pointers to arena memory after the frame. By default the arena keeps its capacity between frames. Set arena_reset_mode in App.Config to change this.

When frames run #

App does not draw continuously. It waits for events. It runs a frame when one of these occurs:

  • Input arrives: pointer, keys, text, scroll, resize, or focus.
  • The previous frame called frame.requestRedraw().
  • A component animates. Animations request frames until they stop.
  • Other code calls app.requestFrame(viewport_id).
  • An async task completes. Read Async work.

When you change data in response to input, call frame.requestRedraw(). Then the next frame shows the change immediately. When a different thread or a timer changes data, call app.requestFrame.

Read input #

Components handle their own input. To read input directly, use frame.ui().input:

const input = &frame.ui().input;
if (input.keyPressed(.escape)) closePanel();
if (input.keyDown(.left_shift)) extendSelection();
if (input.mouseButton(.left).pressed) startDrag();
Function True when
keyPressed(key) The key went down in this frame.
keyRepeated(key) The key sent a repeat in this frame.
keyReleased(key) The key went up in this frame.
keyDown(key) The key is down.
mouseButton(button) Returns the button state, with pressed and other fields.

A component that uses a key event can call input.consumeKeyboard(). Then later components do not see the event.

To act only when a component has focus, compare its ID with frame.ui().focused(id):

const field: ui.Key = .str("search");
const submit = frame.ui().focused(field.hash()) and frame.ui().input.keyPressed(.enter);
try frame.e(TextInput{ .key = field, .buf = &query });
if (submit) runSearch();

frame.input() returns the raw FrameInput for the frame: the logical and physical window size, the content scale, the time, and the time since the last frame.

Focus and keyboard navigation #

Interactive components can take focus. The user moves focus with Tab and Shift+Tab, in the order that the components are emitted. Buttons, checkboxes, and radio buttons react to Enter and Space. A slider with steps greater than zero reacts to the arrow keys.

Effects #

Some actions change the platform, not the UI. The frame collects them, and the host applies them after the frame:

Call Effect
frame.requestRedraw() Run one more frame.
frame.requestClose() Close the window.
frame.writeClipboard(text) Put text on the clipboard.
frame.pasteText() Returns pasted text in this frame, or null.
frame.droppedPaths() Returns file paths that the user dropped on the window in this frame.

The cursor shape is also an effect. Components set it, for example the text cursor over a text field. App applies all effects for you. An embedded host must apply them. Read Embedding.