Skip to content

Guide

Components #

A component is a Zig struct that describes one part of the interface. You make new component values each frame. Knots keeps the state that must stay between frames.

Emit components #

A *ui.Frame has three ways to emit components:

Call Use
frame.e(tree) Emit a component, or a tree of components. Returns nothing.
frame.interact(component) Emit one interactive component and return its response.
component.open(frame) … component.close(frame) Open a parent, emit children with statements, then close the parent.

Trees #

e accepts a tuple. In the tuple, a component followed by a tuple is a parent, and the second tuple holds its children.

try frame.e(.{
    Rect{ .key = .src(@src()), .style = &.{ .direction = .column, .gap = 8 } },
    .{
        Text{ .key = .src(@src()), .content = "Title" },
        Text{ .key = .src(@src()), .content = "Body" },
    },
});

The tree can also contain these items:

  • A function fn (*ui.Frame) anyerror!void. Knots calls it in place.
  • A struct with a render function. This is a custom component.
  • A control-flow value from ui.control: If, For(T), or VirtualList(T). Read Control flow.

Responses #

interact returns the response of the component for this frame. Check it directly after the call.

const response = try frame.interact(ui.component.Button{
    .key = .src(@src()),
    .label = "Save",
});
if (response.clicked) try save();

Open and close #

Use open and close when the children need loops, conditions, or responses. Each open must have one close.

const row: ui.component.Rect = .{ .key = .src(@src()), .style = &.{ .gap = 8 } };
_ = try row.open(frame);
for (items) |item| try renderItem(frame, item);
try row.close(frame);

Common fields #

All built-in components use the same field names:

  • key: ui.Key — required. The identity of the component. Read Keys.
  • style: *const ui.Style — optional. Changes the root element. Read Styles and themes.
  • parts — optional, on components that have more than one element. Each part is a *const ui.Style. For example, Checkbox has box, indicator, and label parts.

Components do not have separate fields for colors, sizes, or padding. All visual changes go through style and parts.

Component catalog #

All components are in ui.component.

Structure #

Component Fields Description
Rect — The basic box. Use it for rows, columns, grids, panels, and scroll areas.
Spacer — Empty space. Set its size with style.
Collapsible open: bool, animation Shows or hides content with an animation. Use openContent, which returns true when the content is visible, and closeContent.

Text and input #

Component Fields Description
Text content, selectable = true Shows text. The user can select and copy it. Part: selection.
TextInput buf: *std.ArrayList(u8), placeholder, bytes_max One line of editable text. Edits buf directly. Parts: placeholder, caret, selection.
TextArea Same as TextInput Many lines of editable text, with a resize grip. Adds the part thumb.
SelectInput(T) values, labels, initial_selected, placeholder A drop-down list. If T is an enum, the values and labels come from the enum. The response has selected: ?Selection.

TextInput and TextArea grow buf with the UI allocator, which is the allocator that you give to App.init. Free buf with the same allocator.

Controls #

Component Fields Response
Button label, disabled clicked, hovered
MenuButton(Menu) menu: Menu, label, close_on_popup_click A button that opens menu in a popup.
Checkbox checked: *bool, label changed
RadioButton(T) selected: *T, value: T, label One option. Sets selected to value.
RadioGroup(T) selected: *T, values, labels A group of radio buttons. Enums supply values and labels.
SliderInput value: *f32, min = 0, max = 1, steps = 0 changed. Parts: track, fill, thumb.
ColorPicker value: *ui.Color changed. Opens a popup with a color area and strips.
ProgressBar progress: f32 (0 to 1) Not interactive. Parts: track, fill.

MenuButton and ContextMenu take a Menu type. A Menu is any component: a struct with open and close functions. Knots renders it inside the popup.

Overlays #

Component Fields Description
Tooltip content, delay_ms = 450, placement Shows a text popup when the pointer stays on its children.
Dialog is_open: *bool, close_on_escape, close_on_backdrop_press A modal panel over a backdrop. closeResponse tells you why it closed.
ContextMenu(Menu) menu: Menu Opens menu at the pointer when the user right-clicks its children.
FloatingWindow is_open: *bool, title, initial_size, resizable, closable, maximizable A window inside the UI that the user can move and resize. Parts: title_bar, title_button, content.

Drawing and media #

Component Fields Description
Canvas commands: []const DrawCmd, interactive Draws shapes in the element: rectangles, gradients, circles, lines, triangles, and convex polygons.
GPUCanvas paint: render.PaintCallback, interactive Calls your function during rendering, so that you can record your own GPU commands in the element area. HMR modules cannot use it.
Image source, sampling_mode Shows a texture or a block of pixels. foreground tints the image.
Graph series, rules, x_domain, y_domain Draws line, bar, and point charts.

Custom components #

A custom component is a struct with a render function. Its fields are the inputs. e calls render when the struct is in a tree.

const Badge = struct {
    key: ui.Key,
    label: []const u8,
    count: usize,

    pub fn render(badge: *const Badge, frame: *ui.Frame) anyerror!void {
        try frame.e(.{
            Rect{ .key = badge.key, .style = &.{ .gap = 6, .@"align" = .center } },
            .{
                Text{ .key = badge.key.indexed(0), .content = badge.label },
                Text{
                    .key = badge.key.indexed(1),
                    .content = try std.fmt.allocPrint(frame.arena(), "{d}", .{badge.count}),
                    .style = &.{ .foreground = .dimmed },
                },
            },
        });
    }
};

try frame.e(Badge{ .key = .src(@src()), .label = "Inbox", .count = 3 });

Take a key field and derive the keys of the children from it with indexed. Then two badges on the same screen do not share keys.

The render signature must be exactly:

pub fn render(self: *const T, frame: *ui.Frame) anyerror!void

To make a custom parent, give the struct open and close functions instead. Then it can have children in a tree.

Control flow #

Normal Zig if and for statements work in a frame function. ui.control also has values that you can put in a tree:

  • If{ .when, .then, .@"else" } calls then or else.
  • For(T){ .items, .each } calls each(frame, item, index) for each item.
  • VirtualList(T){ .key, .items, .row_height, .each, .overscan = 4 } renders only the rows that are visible in a scroll area. Use it for long lists. All rows must have the same height.
try frame.e(.{
    Rect{ .key = .src(@src()), .style = &.{ .height = .fixed(320), .direction = .column, .overflow = .scroll_y } },
    .{
        ui.control.VirtualList(usize){
            .key = .src(@src()),
            .items = row_ids,
            .row_height = 22,
            .each = renderRow,
        },
    },
});