A colour from 0 to 255 channels.
Whether the platform's main shortcut modifier is held.
#import "Extensions/UI";
A native user interface library for jaic programs.
Windows, rendering and text are our own: the window comes from Window_Creation, drawing from
Extensions/WebGPU (Metal, Vulkan or D3D12 natively, the page's WebGPU in the playground) and
text from FreeType into a glyph atlas. The widget tree is retained and keyed but declared with
plain calls every frame, so the app owns its data and the library owns scroll offsets, focus and
cursors. Containers are for blocks.
#import "Basic";
#import "Extensions/UI";
App :: struct { count: int; }
main :: () {
app: App;
run_app(.{title = "Counter", width = 320, height = 200}, *app, build);
}
build :: (app: *App) {
for column(.{gap = 8, padding = 16}) {
label(tprint("Clicked % times", app.count));
if button("Click me") app.count += 1;
}
}This one is only checked to compile, not run.
Tests drive the same build procedures without a window or a GPU (headless, frame, click,
layout_dump).
A colour from 0 to 255 channels.
A colour from a 0xRRGGBB number.
color with its alpha replaced.
solid := rgb(10, 20, 30);
assert(with_alpha(solid, 0.25).w == 0.25);↗Whether the point lies inside rect.
The overlap of two rectangles (zero-sized when they do not touch).
rect shrunk by amount on every side.
value rounded to the nearest whole number (halves go up).
The built-in theme for mode.
ui := headless(.{ theme = .LIGHT });
defer free_ui(*ui);
assert(ui.theme.background == default_theme(.LIGHT).background);↗Replace the theme of the UI being built.
The theme of the UI being built.
Open the font file at path into the next slot. Returns false when it cannot be read or parsed.
ui := headless();
defer free_ui(*ui);
if !load_font(*ui.text, "fonts/Inter-Regular.ttf") print("font missing\n");↗This one is only checked to compile, not run.
Open a font from bytes (which the text system keeps and frees).
ui := headless();
defer free_ui(*ui);
#import "File";
data := read_entire_file("fonts/Inter-Regular.ttf");
load_font_data(*ui.text, data, "Inter");↗This one is only checked to compile, not run.
Add a fixed-metrics font (no glyphs are drawn) to the next slot.
ui := headless();
defer free_ui(*ui);
width := text_width(*ui.text, "abcde", .{ size = 10 });
assert(width == 30); // every character is 0.6 of the size wide↗Load the platform's usual fonts: a sans font into slot 0, a monospace font into slot 1, then a
few fallbacks for symbols and CJK. Any that are missing are replaced by synthetic fonts for the
first two slots. The environment variables JAI_UI_FONT and JAI_UI_MONO_FONT name font files
that replace the sans and monospace choices.
ui := headless();
defer free_ui(*ui);
load_system_fonts(*ui.text);↗This one is only checked to compile, not run.
Release every font and the glyph cache.
Set the device scale; glyphs rasterized at another scale stay in the cache under their own size.
Ascent, descent and line height of a font slot at size points.
ui := headless();
defer free_ui(*ui);
metrics := font_metrics(*ui.text, .SANS, 20);
assert(metrics.line_height > 0);↗The width in points of string drawn with style, without kerning.
ui := headless();
defer free_ui(*ui);
assert(text_width(*ui.text, "ten chars!", .{ size = 10 }) == 60);↗Horizontal advance of one code point in points (a tab is four spaces).
ui := headless();
defer free_ui(*ui);
assert(glyph_advance(*ui.text, #char "a", .{ size = 10 }) == 6);↗The byte offset in string_to_measure of the grapheme boundary nearest to horizontal position x.
ui := headless();
defer free_ui(*ui);
index := index_at_x(*ui.text, "abcdef", .{ size = 10 }, 13); // x = 13 falls in the third character
assert(index == 2);↗The glyph for code in a font slot at size points, rasterized into the atlas on first use.
Falls back through the later fonts when the slot's font has no glyph for it.
This one is only checked to compile, not run.
Forget the cached glyphs and clear the atlas, when it ran out of room. Called at the start of a frame.
Whether the platform's main shortcut modifier is held.
Queue a pointer move to (x, y) in points.
Queue a button press at the position of the last queued move.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
node := find_label(*ui, "Add");
send_mouse_move(*ui, node.rect.x + 2, node.rect.y + 2);
send_mouse_down(*ui);
send_mouse_up(*ui);
frame(*ui, *count, build);
assert(count == 1);↗Queue a button release at the position of the last queued move.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
node := find_label(*ui, "Add");
send_mouse_move(*ui, node.rect.x + 2, node.rect.y + 2);
send_mouse_down(*ui);
send_mouse_up(*ui);
frame(*ui, *count, build);
assert(count == 1);↗Queue wheel or trackpad scrolling; positive dy scrolls the content up (towards its end).
ui := headless(.{ width = 200, height = 100 });
defer free_ui(*ui);
build := (u: *UI_State) {
for scroll(.{ height = 100 }) { for 0..29 label(tprint("line %", it)); }
};
frame(*ui, *ui, build);
send_mouse_move(*ui, 50, 50);
send_scroll(*ui, 0, -3);
frame(*ui, *ui, build);↗Queue a key press.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hi");
ui := headless();
defer free_ui(*ui);
build := (e: *Text_Edit) { for column() { text_field(e); } };
frame(*ui, *edit, build);
click(*ui, find_role(*ui, .TEXT_FIELD));
frame(*ui, *edit, build);
move_caret(*edit, 2);
send_key(*ui, .BACKSPACE);
frame(*ui, *edit, build);
assert(contents(*edit) == "h");↗Queue typed text, one event per code point.
edit: Text_Edit;
defer free_text_edit(*edit);
ui := headless();
defer free_ui(*ui);
build := (e: *Text_Edit) { for column() { text_field(e); } };
frame(*ui, *edit, build);
click(*ui, find_role(*ui, .TEXT_FIELD));
frame(*ui, *edit, build);
send_text(*ui, "hello");
frame(*ui, *edit, build);
assert(contents(*edit) == "hello");↗The UI being built (context.ui).
Release everything a UI owns.
Start a frame: reset the per-frame arena, clear the display list and open the root container.
Finish the frame: drop nodes nobody declared, lay the tree out and fix focus.
ui := headless(.{ width = 200, height = 100 });
defer free_ui(*ui);
push_ui(*ui);
begin_frame(*ui);
button("OK");
end_frame(*ui);
pop_ui();
assert(find_label(*ui, "OK").rect.w > 0);↗Run one frame with the app's build procedure.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
click(*ui, find_label(*ui, "Add"));
frame(*ui, *count, build);
assert(count == 1);↗Make ui the current UI for the code that follows (a second window, or tests).
Restore the UI that was current before the matching push_ui.
Give the next widget an explicit key, for items in a list whose order changes. Without it a widget in a loop is identified by its position in the loop.
The retained value of type T stored on a node, created zeroed on first use and kept until the
node leaves the tree.
Declare a widget: find or create its node, make it the next child of the open container and return it. This is what the library's own widgets use; a custom widget calls it too.
Open a container as the current parent; end_container closes it. Containers written with for do both.
ui := headless();
defer free_ui(*ui);
push_ui(*ui);
begin_frame(*ui);
begin_container(.VERTICAL, .GROUP, .{ gap = 4 }, #location());
label("inside");
end_container(*ui);
end_frame(*ui);
pop_ui();
assert(find_role(*ui, .TEXT).text == "inside");↗Close the innermost open container.
ui := headless();
defer free_ui(*ui);
push_ui(*ui);
begin_frame(*ui);
begin_container(.HORIZONTAL, .GROUP, .{}, #location());
end_container(*ui);
end_frame(*ui);
pop_ui();↗The allocator of the per-frame arena: memory that lives until the next frame begins.
ui := headless();
defer free_ui(*ui);
scratch := alloc(16,, frame_allocator(*ui)); // freed when the next frame begins
assert(scratch != null);↗A copy of text that lives until the next frame, for strings a node keeps.
ui := headless();
defer free_ui(*ui);
copy := frame_string(*ui, "kept until the next frame");
assert(copy == "kept until the next frame");↗What happened to node this frame.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
events := events_of(*ui, find_label(*ui, "Add"));
assert(!events.clicked);↗Give keyboard focus to node.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
focus_node(*ui, find_label(*ui, "Add"));
frame(*ui, *count, build);
assert(events_of(*ui, find_label(*ui, "Add")).focused);↗Ask for another frame (animation, a blinking caret).
// inside a build procedure, while something animates:
request_frame();↗This one is only checked to compile, not run.
The frame's current time in seconds.
This one is only checked to compile, not run.
Measure and arrange the whole tree for the window's size. end_frame calls it; call it again
only after changing a node by hand.
ui := headless(.{ width = 200, height = 100 });
defer free_ui(*ui);
push_ui(*ui);
begin_frame(*ui);
button("OK");
while ui.stack.count > 1 end_container(*ui);
layout_tree(*ui); // end_frame does this for you
pop_ui();
assert(find_label(*ui, "OK").rect.w > 0);↗A focusable widget with no room is reported once: it cannot be seen or clicked, and nothing else says why. The usual cause is
a column or row that is not given grow, so its children get only their natural size and a widget that has none (a text
editor) gets nothing. Called by end_frame when DEBUG_WARNINGS is on.
Release the display list.
list: Draw_List;
free_draw_list(*list);↗Empty the display list for a new frame.
Fill rect with color; radius rounds the corners and border draws an outline inside.
Draw one line of text with its top left at (x, y) and return its width in points.
Draw one line of text with its baseline at baseline, returning its width in points.
ui := headless();
defer free_ui(*ui);
width := draw_text_baseline(*ui, 10, 30, "hello", .{ size = 10 });
assert(width == 30);↗Restrict drawing to rect (intersected with the clip already in force) until pop_clip.
End the innermost push_clip.
Close the container when its for body is done.
A container that lays its children out left to right.
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
for row(.{ gap = 10 }) {
label("left");
label("right");
}
});
assert(find_label(*ui, "right").rect.x > find_label(*ui, "left").rect.x);↗A container that lays its children out top to bottom.
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
for column(.{ gap = 10 }) {
label("top");
label("bottom");
}
});
assert(find_label(*ui, "bottom").rect.y > find_label(*ui, "top").rect.y);↗A container whose children overlap, each filling it (or placed by align).
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
for stack() {
label("under");
label("over");
}
});
assert(find_label(*ui, "under").rect.y == find_label(*ui, "over").rect.y);↗A column on the theme's panel colour.
A column whose content scrolls vertically (horizontally with axis = .HORIZONTAL) when it is
larger than the space it is given. Give it grow = 1 or a fixed size.
One line of text.
A push button. Returns true on the frame it was clicked (or Enter or Space pressed while focused).
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
click(*ui, find_label(*ui, "Add"));
frame(*ui, *count, build);
assert(count == 1);↗A checkbox with a caption. Returns true on the frame the value changed.
A row in a list or tree: highlighted when selected, indented by indent points. Returns true
when clicked.
selected := -1;
ui := headless();
defer free_ui(*ui);
build := (choice: *int) {
for column() {
for 0..2 {
if list_item(tprint("Item %", it), choice.* == it) choice.* = it;
}
}
};
frame(*ui, *selected, build);
click(*ui, find_label(*ui, "Item 1"));
frame(*ui, *selected, build);
assert(selected == 1);↗A horizontal rule in a column (a vertical one in a row).
Empty space that grows to fill what is left in its row or column.
A row of tabs.
One tab in a tab_bar, drawn selected when active.
current := 0;
ui := headless();
defer free_ui(*ui);
build := (selected: *int) {
for tab_bar() {
if tab("One", selected.* == 0) == .SELECT selected.* = 0;
if tab("Two", selected.* == 1) == .SELECT selected.* = 1;
}
};
frame(*ui, *current, build);
click(*ui, find_label(*ui, "Two"));
frame(*ui, *current, build);
assert(current == 1);↗Two panes with a draggable divider. position is the first pane's size along axis
(HORIZONTAL puts the panes side by side); the app keeps it so it can be saved. Use
for pane: split(.HORIZONTAL, *width) { if pane == 0 { ... } else { ... } }.
Draws and drags the divider between split panes.
// split() draws it for you; a custom layout calls it between its own panes
width := 150.0;
for pane: split(.HORIZONTAL, *width) { label("pane"); }↗This one is only checked to compile, not run.
Release the text and its history.
edit: Text_Edit;
set_contents(*edit, "text");
free_text_edit(*edit);↗Replace everything with text and put the caret at the start. History is cleared.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "one\ntwo");
assert(contents(*edit) == "one\ntwo");↗The whole text, as a copy in temporary storage.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "abc");
assert(contents(*edit) == "abc");↗Whether any text is selected.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "abc");
assert(!has_selection(*edit));
select_all(*edit);
assert(has_selection(*edit));↗The selected range as (start, end) byte offsets.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hello world");
move_caret(*edit, 6);
move_caret(*edit, 11, extend = true);
start, end := selection_range(*edit);
assert(start == 6 && end == 11);↗The selected text, valid until the next edit.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hello world");
move_caret(*edit, 6);
move_caret(*edit, 11, extend = true);
assert(selected_text(*edit) == "world");↗Where the caret is, as a zero-based line and a byte column within it.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "ab\ncd");
move_caret(*edit, 4);
line, column := caret_position(*edit);
assert(line == 1 && column == 1);↗Put the caret at offset (clamped), selecting from the old anchor when extend is set.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "abc");
move_caret(*edit, 100); // clamped
assert(edit.cursor == 3);↗Select all the text.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "abc");
select_all(*edit);
assert(selected_text(*edit) == "abc");↗Replace the selection (or insert at the caret) with text.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hello");
select_all(*edit);
insert_text(*edit, "bye"); // replaces the selection
assert(contents(*edit) == "bye");↗Delete the selection. Returns whether there was one.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hello world");
move_caret(*edit, 5);
move_caret(*edit, 11, extend = true);
assert(delete_selection(*edit));
assert(contents(*edit) == "hello");
assert(!delete_selection(*edit));↗Offset one unit to the left (direction -1) or right (1) of from.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "ab cd");
assert(offset_by(*edit, 0, .CHARACTER, 1) == 1);
assert(offset_by(*edit, 5, .WORD, -1) == 3);↗Move the caret one unit left or right.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "one two");
move_by(*edit, .WORD, 1);
assert(edit.cursor == 3);
move_by(*edit, .DOCUMENT, 1, extend = true);
assert(selected_text(*edit) == " two");↗Delete one unit before (direction -1) or after (1) the caret, or the selection if there is one.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "one two");
move_caret(*edit, 7);
delete_by(*edit, .WORD, -1);
assert(contents(*edit) == "one ");↗Move the caret lines lines up (negative) or down, keeping its horizontal position.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "abcd\nef");
move_caret(*edit, 3);
ui := headless();
defer free_ui(*ui);
move_vertically(*ui, *edit, 1, .{});
line, column := caret_position(*edit);
assert(line == 1);↗Select the word around offset.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hello world");
select_word_at(*edit, 8);
assert(selected_text(*edit) == "world");↗Select the whole line containing offset, including its line break.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "one\ntwo\nthree");
select_line_at(*edit, 5);
assert(selected_text(*edit) == "two\n");↗Undo the last step and put the caret where it belongs. Returns whether anything changed.
edit: Text_Edit;
defer free_text_edit(*edit);
insert_text(*edit, "typed");
assert(undo_edit(*edit));
assert(contents(*edit) == "");
assert(!undo_edit(*edit));↗Redo the last undone step. Returns whether anything changed.
edit: Text_Edit;
defer free_text_edit(*edit);
insert_text(*edit, "typed");
undo_edit(*edit);
assert(redo_edit(*edit));
assert(contents(*edit) == "typed");↗Insert a line break, repeating the current line's leading blanks.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, " indented");
move_caret(*edit, 12);
insert_newline(*edit);
assert(contents(*edit) == " indented\n ");↗Insert spaces up to the next multiple of width columns.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "ab");
move_caret(*edit, 2);
insert_tab(*edit, 4);
assert(contents(*edit) == "ab ");↗Apply one KEY or TEXT input event to edit. style is the text style the lines are drawn in
(vertical movement measures with it); page_lines is how many lines a page is.
edit: Text_Edit;
defer free_text_edit(*edit);
ui := headless();
defer free_ui(*ui);
event: Input_Event;
event.kind = .TEXT;
event.character = #char "h";
result := handle_key(*ui, *edit, event, .{}, .{}, 10);
assert(result.changed && contents(*edit) == "h");↗Copy the selection to the clipboard.
edit: Text_Edit;
defer free_text_edit(*edit);
ui := headless();
defer free_ui(*ui);
copy_selection(*ui, *edit); // puts the selection on the clipboard↗This one is only checked to compile, not run.
Copy the selection to the clipboard and delete it.
edit: Text_Edit;
defer free_text_edit(*edit);
ui := headless();
defer free_ui(*ui);
cut_selection(*ui, *edit); // copies the selection, then deletes it↗This one is only checked to compile, not run.
Replace the selection with the clipboard's text.
edit: Text_Edit;
defer free_text_edit(*edit);
ui := headless();
defer free_ui(*ui);
paste_clipboard(*ui, *edit); // inserts the clipboard text at the caret↗This one is only checked to compile, not run.
A multi-line text editor over edit: line numbers, selection, scrolling, undo and the clipboard.
Returns true on the frame the text changed. It fills the space it is given.
#import "Basic";
#import "Extensions/UI";
Notes :: struct {
edit: Text_Edit;
dirty: bool;
}
main :: () {
notes: Notes;
run_app(.{title = "Notes", width = 480, height = 320}, *notes, build);
}
build :: (notes: *Notes) {
if text_editor(*notes.edit, .{line_numbers = true}) notes.dirty = true;
}↗This one is only checked to compile, not run.
A single-line text field over edit. Returns whether the text changed and whether Enter was
pressed.
name: Text_Edit;
defer free_text_edit(*name);
ui := headless();
defer free_ui(*ui);
build := (e: *Text_Edit) {
for column() {
changed, submitted := text_field(e, .{ placeholder = "Your name" });
}
};
frame(*ui, *name, build);
click(*ui, find_role(*ui, .TEXT_FIELD));
frame(*ui, *name, build);
send_text(*ui, "Ada");
frame(*ui, *name, build);
assert(contents(*name) == "Ada");↗The visible rows of an editor as (first line, last line, line height), from its last layout.
edit: Text_Edit;
defer free_text_edit(*edit);
ui := headless();
defer free_ui(*ui);
node := find_role(*ui, .TEXT_EDITOR);
first, last, line_height := visible_lines(*ui, *edit, node, .{});↗This one is only checked to compile, not run.
Ask for a frame in seconds even if no input arrives (animation, a blinking caret).
A UI that is not attached to a window. Fonts are synthetic (every character is 0.6 of the font
size wide) unless system_fonts is set, so layouts are the same on every machine. Release it
with free_ui.
Change the logical size of the window a UI draws into.
The first node matching query, in tree order, or null.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
text := find(*ui, .{ text = "Clicked 0 times" });
assert(text != null);↗The first node with this label.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
assert(find_label(*ui, "Add") != null);
assert(find_label(*ui, "Remove") == null);↗The nth node with this role.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
assert(find_role(*ui, .TEXT).text == "Clicked 0 times");
assert(find_role(*ui, .BUTTON) != null);↗Move the pointer to the centre of node and click it. The next frame applies it and shows the result. node must come
from a frame that has already laid it out.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
click(*ui, find_label(*ui, "Add"));
frame(*ui, *count, build);
assert(count == 1);↗Click at a point in the window.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
button_node := find_label(*ui, "Add");
click_at(*ui, button_node.rect.x + 2, button_node.rect.y + 2);
frame(*ui, *count, build);
assert(count == 1);↗The tree as indented text: role, label or text, and the rectangle of each node. Useful as a snapshot of a layout.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
dump := layout_dump(*ui);
assert(dump.count > 0);
print("%", dump);↗Create a renderer drawing into window's surface. Returns null if no GPU or surface is available.
#import "Window_Creation";
window := create_window(800, 600, "ui");
renderer := create_window_renderer(window, 800, 600);↗This one is only checked to compile, not run.
Create a renderer drawing into an offscreen texture of width by height pixels, for tests and
screenshots. Returns null if no adapter is available.
ui := headless();
defer free_ui(*ui);
renderer := create_offscreen_renderer(640, 480);
defer destroy_renderer(renderer);
image := render_to_image(renderer, *ui, rgb(20, 20, 30));↗This one is only checked to compile, not run.
Release the renderer and its GPU objects.
renderer := create_offscreen_renderer(320, 200);
destroy_renderer(renderer);↗This one is only checked to compile, not run.
Match the target to a new size in pixels (a window resize).
renderer := create_offscreen_renderer(320, 200);
resize_target(renderer, 640, 400);↗This one is only checked to compile, not run.
Draw the UI's display list into the window. Returns false when there was nothing to draw into (the window is hidden) and the frame should be tried again.
#import "Window_Creation";
ui := headless();
defer free_ui(*ui);
window := create_window(800, 600, "ui");
renderer := create_window_renderer(window, 800, 600);
present(renderer, *ui, rgb(20, 20, 30));↗This one is only checked to compile, not run.
Draw the UI's display list offscreen and read the pixels back. Free pixels with array_free.
ui := headless();
defer free_ui(*ui);
renderer := create_offscreen_renderer(320, 200);
defer destroy_renderer(renderer);
image := render_to_image(renderer, *ui, rgb(255, 255, 255));↗This one is only checked to compile, not run.
Draw the UI's last frame offscreen and write it to path as a PNG. Needs a GPU; build the UI
with system_fonts for readable text. Returns false when there is no adapter or the file
cannot be written.
ui := headless();
defer free_ui(*ui);
write_screenshot(*ui, "frame.png");↗This one is only checked to compile, not run.
Open a window and run build for every frame until the window closes. app is passed to build
each frame. Returns false when the window or the GPU could not be set up.
#import "Basic";
#import "Extensions/UI";
Counter :: struct {
count: int;
}
main :: () {
app: Counter;
run_app(.{title = "Counter", width = 320, height = 200}, *app, build);
}
build :: (app: *Counter) {
if button(tprint("Clicked % times", app.count)) app.count += 1;
}↗This one is only checked to compile, not run.
Like run_app, for a UI the caller owns (to set the theme or hooks first, or to inspect it after).
main :: () {
ui := headless();
app: int;
run_app_with(.{ title = "Custom UI" }, *ui, *app, (n: *int) { label("hi"); });
}↗This one is only checked to compile, not run.
An axis-aligned rectangle: x, y is the top left corner.
Which way a container lays out its children.
axis := Axis.HORIZONTAL;
assert(axis != .VERTICAL);↗How a container places children across its axis.
style := Style.{ align = .CENTER };
assert(style.align == .CENTER);↗How a container places children along its axis when none of them grow.
style := Style.{ justify = .SPACE_BETWEEN };
assert(style.justify == .SPACE_BETWEEN);↗What a widget is, kept on its node for tests, for tools and for a later accessibility export.
Layout and paint options for a container or widget. Zero values mean "automatic" or "none", so a
style is written with only what differs: .{gap = 8, padding = 12}.
padding on the left and rightpadding on the top and bottomLight or dark defaults for default_theme.
light := default_theme(.LIGHT);
dark := default_theme(.DARK);
assert(light.background != dark.background);↗The look of the library's widgets. Change fields on ui.theme (or set_theme) to restyle everything.
t := default_theme();
t.accent = hex(0xFF5500);Which font a piece of text uses.
style := Text_Style.{ font = .MONO, size = 14 };
assert(style.font == .MONO);↗How text is drawn.
style := Text_Style.{ size = 16 };
assert(style.size == 16);↗A loaded font file. Created by load_font and owned by the Text_System.
Where a glyph is in the atlas and how to place it.
Vertical metrics in points.
The glyph atlas: a one-channel texture filled row by row, uploaded by the renderer when
version changes.
Everything text needs: fonts, the glyph cache and the atlas.
Mouse buttons.
button := Mouse_Button.RIGHT;
assert(button != .LEFT);↗Which modifier keys are held. primary is Cmd on macOS and Ctrl elsewhere.
mods := Modifiers.{ ctrl = true };
assert(mods.ctrl && !mods.shift);↗What an input event is.
keycharacterkind := Input_Kind.KEY;
assert(kind != .TEXT);↗A queued input event. Keys use the Input module's Key_Code (letters are their upper case ASCII).
The pointer, modifiers and the keyboard events a frame sees.
How a node lays out and paints.
paintA procedure that paints a node's rectangle. It may use ui.draw freely.
One widget in the tree. App code reads it through widget's result and the test helpers; the
library fills it in.
key call that named this node this frame, for warnings (debug only)stateWhat happened to a node this frame, from the input events routed to it.
ui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
node := find_label(*ui, "Add");
click(*ui, node);
frame(*ui, *count, build);
events := events_of(*ui, find_label(*ui, "Add"));
assert(events.hovered);↗All the state of one UI: its tree, fonts, input and the frame being drawn. Create one with
headless or let run_app make it.
Platform services the library needs but does not own, set by run_app. Tests leave them null.
The mouse cursor shapes widgets ask for.
What a display list entry draws.
colorOne display list entry.
The display list of a frame plus the clip stack used while building it.
An open container, returned by row, column and the other containers so for can close it.
Options for text: all optional.
Looks of a button.
Options for button.
What a click on a tab did.
result := Tab_Result.NONE;
assert(result != .SELECT && result != .CLOSE);↗Options for tab.
An open split: the two panes are visited with for pane: split(...).
Options for split.
options := Split_Options.{ min = 100, max = 300 };
assert(options.min == 100);↗A piece of text being edited. Zero is empty; release it with free_text_edit.
cursor when nothing is selected)edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "hello");
move_caret(*edit, 5);
insert_text(*edit, " world");
assert(contents(*edit) == "hello world");↗A coloured range of one line, produced by an editor's highlight procedure.
paint_comments :: (line: string, spans: *[..] Highlight_Span) {
if line.count >= 2 && line[0] == #char "/" && line[1] == #char "/" {
array_add(spans, .{ 0, line.count, rgb(120, 140, 120) });
}
}↗This one is only checked to compile, not run.
A procedure that colours one line of text by appending spans (unlisted text uses the default colour).
paint_comments :: (line: string, spans: *[..] Highlight_Span) {
if line.count >= 2 && line[0] == #char "/" && line[1] == #char "/" {
array_add(spans, .{ 0, line.count, rgb(120, 140, 120) });
}
}
options := Editor_Options.{ highlight = paint_comments };↗This one is only checked to compile, not run.
A range of the text drawn with a background: search results, diagnostics, matching brackets.
marks := Text_Mark.[.{ start = 4, end = 9, color = rgb(255, 220, 0, 90) }];
options := Editor_Options.{ marks = marks };↗This one is only checked to compile, not run.
Options for text_editor and text_field.
options := Editor_Options.{ line_numbers = true, tab_width = 2 };↗This one is only checked to compile, not run.
What a movement or deletion covers.
edit: Text_Edit;
defer free_text_edit(*edit);
set_contents(*edit, "one two");
assert(offset_by(*edit, 0, .WORD, 1) == 3);
assert(offset_by(*edit, 0, .DOCUMENT, 1) == 7);↗What handle_key did.
result := Key_Result.{ changed = true };
assert(result.changed && !result.submitted);↗Options for headless.
How find picks a node: every field that is set must match.
role countsui := headless(.{ width = 320, height = 200 });
defer free_ui(*ui);
count := 0;
build := (n: *int) {
for column(.{ gap = 8, padding = 16 }) {
label(tprint("Clicked % times", n.*));
if button("Add") n.* += 1;
}
};
frame(*ui, *count, build);
node := find(*ui, .{ role = .BUTTON, match_role = true });
assert(node != null && node.label == "Add");
assert(find(*ui, .{ text = "no such text" }) == null);↗A renderer bound to one window or to an offscreen target.
One entry of the instance buffer, as the shader reads it.
A run of instances drawn with one scissor rectangle.
Pixels read back from an offscreen render: rows top to bottom, 4 bytes per pixel (R, G, B, A).
Options for run_app.
options := App_Options.{ title = "Notes", width = 900, height = 600 };↗This one is only checked to compile, not run.
What run_app can tell the app about the window.
A colour with straight (not premultiplied) alpha, each channel 0 to 1.