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
renderfunction. This is a custom component. -
A control-flow value from
ui.control:If,For(T), orVirtualList(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,Checkboxhasbox,indicator, andlabelparts.
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" }callsthenorelse. -
For(T){ .items, .each }callseach(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,
},
},
});