Modules

UI

moduleextensionexperimentalSource

Available on macOS, Linux, Windows·since 0.31.0

#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.

Example↗
#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).

Module parameters#

DEBUG_WARNINGS := true
Warns about layout mistakes, such as focusable elements with no size.

Procedures#

Types#

rgbexperimentalsince 0.31.0Source
(r: int, g: int, b: int, a: int = 255) -> Color

A colour from 0 to 255 channels.

c := rgb(255, 128, 0);
assert(c.x == 1 && c.w == 1);
faded := rgb(255, 128, 0, 0);
assert(faded.w == 0);
↗
hexexperimentalsince 0.31.0Source
(value: u32, alpha: float = 1) -> Color

A colour from a 0xRRGGBB number.

white := hex(0xFFFFFF);
assert(white.x == 1 && white.y == 1 && white.z == 1);
glass := hex(0x3B82F6, 0.5);
assert(glass.w == 0.5);
↗
containsexperimentalsince 0.31.0Source
(rect: Rect, x: float, y: float) -> bool

Whether the point lies inside rect.

r := Rect.{ 0, 0, 10, 10 };
assert(contains(r, 0, 0));
assert(!contains(r, 10, 10));
↗
intersectexperimentalsince 0.31.0Source
(a: Rect, b: Rect) -> Rect

The overlap of two rectangles (zero-sized when they do not touch).

overlap := intersect(.{ 0, 0, 10, 10 }, .{ 5, 5, 10, 10 });
assert(overlap.x == 5 && overlap.w == 5 && overlap.h == 5);
assert(intersect(.{ 0, 0, 1, 1 }, .{ 5, 5, 1, 1 }).w == 0);
↗
insetexperimentalsince 0.31.0Source
(rect: Rect, amount: float) -> Rect

rect shrunk by amount on every side.

r := inset(.{ 0, 0, 100, 40 }, 10);
assert(r.x == 10 && r.w == 80 && r.h == 20);
↗
roundexperimentalsince 0.31.0Source
(value: float) -> float

value rounded to the nearest whole number (halves go up).

assert(round(2.5) == 3);
assert(round(2.4) == 2);
↗

Theme#

Text#

load_fontexperimentalsince 0.31.0Source
(text: *Text_System, path: string) -> bool

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.

load_font_dataexperimentalsince 0.31.0Source
(text: *Text_System, data: string, name: string = "") -> bool

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.

load_system_fontsexperimentalsince 0.31.0Source
(text: *Text_System)

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.

set_scaleexperimentalsince 0.31.0Source
(text: *Text_System, scale: float)

Set the device scale; glyphs rasterized at another scale stay in the cache under their own size.

ui := headless();
defer free_ui(*ui);
set_scale(*ui.text, 2);  // a Retina display
↗
index_at_xexperimentalsince 0.31.0Source
(text: *Text_System, string_to_measure: string, style: Text_Style, x: float) -> int

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);
↗
get_glyphexperimentalsince 0.31.0Source
(text: *Text_System, code: u32, slot: Font_Slot, size: float) -> Glyph

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.

ui := headless();
defer free_ui(*ui);
glyph := get_glyph(*ui.text, #char "A", .SANS, 16);
↗

This one is only checked to compile, not run.

Input#

primaryexperimentalsince 0.31.0Source
(mods: Modifiers) -> bool

Whether the platform's main shortcut modifier is held.

mods := Modifiers.{ ctrl = true, shift = true };
shortcut := primary(mods);  // Ctrl on Windows and Linux, Cmd on macOS
↗
send_mouse_downexperimentalsince 0.31.0Source
(ui: *UI_State, button: Mouse_Button = .LEFT)

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);
↗
send_mouse_upexperimentalsince 0.31.0Source
(ui: *UI_State, button: Mouse_Button = .LEFT)

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);
↗
send_scrollexperimentalsince 0.31.0Source
(ui: *UI_State, dx: float, dy: float)

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);
↗
send_keyexperimentalsince 0.31.0Source
(ui: *UI_State, key: Key_Code, mods: Modifiers = .{}, repeat: bool = false)

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");
↗

Tree#

frameexperimentalsince 0.31.0Source
(ui: *UI_State, app: *$T, build: (app: *T))

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);
↗
keyexperimentalsince 0.31.0Source
(value: $T)

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.

Item :: struct { id: int; name: string; }
items := Item.[.{ 7, "seven" }, .{ 9, "nine" }];

ui := headless();
defer free_ui(*ui);
frame(*ui, *items, (list: *[2] Item) {
    for column() {
        for list.* {
            key(it.id);
            label(it.name);
        }
    }
});
assert(find_label(*ui, "nine") != null);
↗
stateexperimentalsince 0.31.0Source
(node: *Node, $T: Type) -> *T

The retained value of type T stored on a node, created zeroed on first use and kept until the node leaves the tree.

ui := headless();
defer free_ui(*ui);
push_ui(*ui);
begin_frame(*ui);
node := widget(.LEAF, .CUSTOM, .{ width = 10, height = 10 }, #location());
clicks := state(node, int);
clicks.* += 1;
assert(state(node, int).* == 1);
end_frame(*ui);
pop_ui();
↗
widgetexperimentalsince 0.31.0Source
(kind: Node_Kind, role: Role, style: Style, loc: Source_Code_Location) -> *Node

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.

ui := headless();
defer free_ui(*ui);
push_ui(*ui);
begin_frame(*ui);
node := widget(.LEAF, .CUSTOM, .{ width = 40, height = 20 }, #location());
node.label = "swatch";
end_frame(*ui);
pop_ui();
assert(node.kind == .LEAF);
↗
frame_allocatorexperimentalsince 0.31.0Source
(ui: *UI_State) -> Allocator

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);
↗
frame_stringexperimentalsince 0.31.0Source
(ui: *UI_State, text: string) -> string

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");
↗
request_frameexperimentalsince 0.31.0Source
()

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.

ui_timeexperimentalsince 0.31.0Source
() -> float64

The frame's current time in seconds.

// inside a build procedure:
seconds := ui_time();
↗

This one is only checked to compile, not run.

Layout#

warn_zero_size_focusablesexperimentalsince 0.31.0Source
(ui: *UI_State)

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.

Draw#

draw_rectexperimentalsince 0.31.0Source
(ui: *UI_State, rect: Rect, color: Color, radius: float = 0, border: float = 0, border_color: Color = .{})

Fill rect with color; radius rounds the corners and border draws an outline inside.

ui := headless();
defer free_ui(*ui);
draw_rect(*ui, .{ 10, 10, 100, 40 }, rgb(40, 40, 60), radius = 6, border = 1, border_color = rgb(90, 90, 120));
assert(ui.draw.cmds.count == 1);
↗
draw_textexperimentalsince 0.31.0Source
(ui: *UI_State, x: float, y: float, text: string, style: Text_Style) -> float

Draw one line of text with its top left at (x, y) and return its width in points.

ui := headless();
defer free_ui(*ui);
width := draw_text(*ui, 10, 10, "hello", .{ size = 10 });
assert(width == 30);
↗

Widgets#

for_expansionexperimental2 overloadssince 0.31.0Source
(container: Container, body: Code, flags: For_Flags)
(s: Split, body: Code, flags: For_Flags)

Close the container when its for body is done.

scrollexperimentalsince 0.31.0Source
(style: Style = .{}, axis: Axis = .VERTICAL, loc := #caller_location) -> Container

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.

ui := headless(.{ width = 200, height = 100 });
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
    for scroll(.{ height = 100 }) {
        for 0..19 label(tprint("line %", it));
    }
});
assert(find_role(*ui, .SCROLL) != null);
↗
buttonexperimentalsince 0.31.0Source
(text: string, options: Button_Options = .{}, loc := #caller_location) -> clicked: bool

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);
↗
checkboxexperimentalsince 0.31.0Source
(value: *bool, text: string = "", loc := #caller_location) -> changed: bool

A checkbox with a caption. Returns true on the frame the value changed.

on := false;
ui := headless();
defer free_ui(*ui);
build := (value: *bool) { checkbox(value, "Dark mode"); };
frame(*ui, *on, build);
click(*ui, find_role(*ui, .CHECKBOX));
frame(*ui, *on, build);
assert(on);
↗
list_itemexperimentalsince 0.31.0Source
(text: string, selected: bool = false, indent: float = 0, loc := #caller_location) -> clicked: bool

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);
↗
spacerexperimentalsince 0.31.0Source
(grow: float = 1, loc := #caller_location)

Empty space that grows to fill what is left in its row or column.

ui := headless(.{ width = 300, height = 40 });
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
    for row(.{ width = 300 }) {
        label("left");
        spacer();  // pushes what follows to the right edge
        label("right");
    }
});
assert(find_label(*ui, "right").rect.x > 200);
↗
tabexperimentalsince 0.31.0Source
(text: string, active: bool, options: Tab_Options = .{}, loc := #caller_location) -> Tab_Result

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);
↗
splitexperimentalsince 0.31.0Source
(axis: Axis, position: *float, options: Split_Options = .{}, loc := #caller_location) -> Split

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 { ... } }.

width := 150.0;
ui := headless(.{ width = 400, height = 200 });
defer free_ui(*ui);
frame(*ui, *width, (w: *float) {
    for pane: split(.HORIZONTAL, w, .{ min = 100 }) {
        if pane == 0 label("files"); else label("editor");
    }
});
assert(width == 150);
↗
dividerexperimentalsince 0.31.0Source
(s: Split, loc := #caller_location)

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.

Text edit#

move_caretexperimentalsince 0.31.0Source
(edit: *Text_Edit, offset: int, extend: bool = false)

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);
↗
handle_keyexperimentalsince 0.31.0Source
(ui: *UI_State, edit: *Text_Edit, event: Input_Event, style: Text_Style, options: Editor_Options, page_lines: int) -> Key_Result

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");
↗
text_editorexperimentalsince 0.31.0Source
(edit: *Text_Edit, options: Editor_Options = .{}, loc := #caller_location) -> changed: bool

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.

text_fieldexperimentalsince 0.31.0Source
(edit: *Text_Edit, options: Editor_Options = .{}, loc := #caller_location) -> (changed: bool, submitted: bool)

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");
↗
visible_linesexperimentalsince 0.31.0Source
(ui: *UI_State, edit: *Text_Edit, node: *Node, options: Editor_Options) -> (first: int, last: int, line_height: float)

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.

Headless#

headlessexperimentalsince 0.31.0Source
(options: Headless_Options = .{}) -> UI_State

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.

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);
↗
resizeexperimentalsince 0.31.0Source
(ui: *UI_State, width: float, height: float)

Change the logical size of the window a UI draws into.

ui := headless();
defer free_ui(*ui);
resize(*ui, 400, 300);
assert(ui.size.x == 400 && ui.size.y == 300);
↗
findexperimentalsince 0.31.0Source
(ui: *UI_State, query: Find_Query) -> *Node

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);
↗
find_labelexperimentalsince 0.31.0Source
(ui: *UI_State, label: string) -> *Node

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);
↗
find_roleexperimentalsince 0.31.0Source
(ui: *UI_State, role: Role, index: int = 0) -> *Node

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);
↗
clickexperimentalsince 0.31.0Source
(ui: *UI_State, node: *Node)

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_atexperimentalsince 0.31.0Source
(ui: *UI_State, x: float, y: float)

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);
↗
layout_dumpexperimentalsince 0.31.0Source
(ui: *UI_State) -> string

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);
↗

Render#

create_window_rendererexperimentalsince 0.31.0Source
(window: Window_Type, width: int, height: int) -> *Renderer

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.

presentexperimentalsince 0.31.0Source
(r: *Renderer, ui: *UI_State, clear: Color) -> bool

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.

write_screenshotexperimentalsince 0.31.0Source
(ui: *UI_State, path: string) -> bool

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.

App#

run_appexperimentalsince 0.31.0Source
(options: App_Options, app: *$T, build: (app: *T)) -> bool

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.

run_app_withexperimentalsince 0.31.0Source
(options: App_Options, ui: *UI_State, app: *$T, build: (app: *T)) -> bool

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.

Types#

Rectstructexperimentalsince 0.31.0Source
:: struct

An axis-aligned rectangle: x, y is the top left corner.

x: float
y: float
w: float
h: float
r := Rect.{ x = 10, y = 20, w = 100, h = 50 };
assert(contains(r, 10, 20));
assert(!contains(r, 110, 20));
↗
Axisenumexperimentalsince 0.31.0Source
:: enum

Which way a container lays out its children.

VERTICAL
a column
HORIZONTAL
a row
STACK
children overlap, each placed in the container's rectangle
axis := Axis.HORIZONTAL;
assert(axis != .VERTICAL);
↗
Alignenumexperimentalsince 0.31.0Source
:: enum

How a container places children across its axis.

STRETCH
fill the container
START
CENTER
END
style := Style.{ align = .CENTER };
assert(style.align == .CENTER);
↗
Justifyenumexperimentalsince 0.31.0Source
:: enum

How a container places children along its axis when none of them grow.

START
CENTER
END
SPACE_BETWEEN
style := Style.{ justify = .SPACE_BETWEEN };
assert(style.justify == .SPACE_BETWEEN);
↗
Roleenumexperimentalsince 0.31.0Source
:: enum

What a widget is, kept on its node for tests, for tools and for a later accessibility export.

GROUP
TEXT
BUTTON
CHECKBOX
TEXT_FIELD
TEXT_EDITOR
SCROLL
SPLIT
TAB_LIST
TAB
LIST_ITEM
TREE_ITEM
SEPARATOR
CUSTOM
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) { button("OK"); });
assert(find_role(*ui, .BUTTON).label == "OK");
↗
Stylestructexperimentalsince 0.31.0Source
:: struct

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}.

gap: float
space between children
padding: float
space inside the edge, all sides
padding_x: float
added to padding on the left and right
padding_y: float
added to padding on the top and bottom
grow: float
share of the space left over in the parent's axis
width: float
fixed width (0: from content)
height: float
fixed height (0: from content)
min_width: float
min_height: float
align: Align
placement of children across the axis
justify: Justify
placement of children along the axis
background: Color
alpha 0: none
border_color: Color
border: float
border width
radius: float
corner radius
clip: bool
clip children to the rectangle
style := Style.{ gap = 8, padding = 12, background = rgb(30, 30, 40), radius = 6 };
assert(style.grow == 0);  // zero means automatic
↗
Theme_Modeenumexperimentalsince 0.31.0Source
:: enum

Light or dark defaults for default_theme.

DARK
LIGHT
light := default_theme(.LIGHT);
dark := default_theme(.DARK);
assert(light.background != dark.background);
↗
Themestructexperimentalsince 0.31.0Source
:: struct

The look of the library's widgets. Change fields on ui.theme (or set_theme) to restyle everything.

background: Color
the window
surface: Color
panels and bars
surface_raised: Color
controls: buttons, fields
surface_hover: Color
surface_pressed: Color
text: Color
text_muted: Color
accent: Color
accent_text: Color
text on the accent colour
outline: Color
selection: Color
caret: Color
error: Color
warning: Color
font_size: float
mono_size: float
radius: float
padding: float
inside a control
gap: float
between controls
t := default_theme();
t.accent = hex(0xFF5500);
Font_Slotenumexperimentalsince 0.31.0Source
:: enum

Which font a piece of text uses.

SANS
the interface font
MONO
the code font
style := Text_Style.{ font = .MONO, size = 14 };
assert(style.font == .MONO);
↗
Text_Stylestructexperimentalsince 0.31.0Source
:: struct

How text is drawn.

font: Font_Slot
size: float
points
color: Color
style := Text_Style.{ size = 16 };
assert(style.size == 16);
↗
Fontstructexperimentalsince 0.31.0Source
:: struct

A loaded font file. Created by load_font and owned by the Text_System.

face: FT_Face
data: [..] u8
the file, which FreeType reads from for the face's lifetime
name: string
Glyphstructexperimentalsince 0.31.0Source
:: struct

Where a glyph is in the atlas and how to place it.

advance: float
pixels
left: float
pixels from the pen to the bitmap's left edge
top: float
pixels from the baseline up to the bitmap's top edge
width: float
bitmap size in pixels
height: float
u0: float
v0: float
u1: float
v1: float
visible: bool
false: nothing to draw (a space)
Font_Metricsstructexperimentalsince 0.31.0Source
:: struct

Vertical metrics in points.

ascent: float
descent: float
line_height: float
Atlasstructexperimentalsince 0.31.0Source
:: struct

The glyph atlas: a one-channel texture filled row by row, uploaded by the renderer when version changes.

width: int
height: int
pixels: [..] u8
pen_x: int
pen_y: int
row_height: int
version: int
changes whenever pixels do
full: bool
ran out of room; the cache is cleared at the start of the next frame
Text_Systemstructexperimentalsince 0.31.0Source
:: struct

Everything text needs: fonts, the glyph cache and the atlas.

library: FT_Library
fonts: [..] *Font
0 is SANS, 1 is MONO, the rest are fallbacks for missing glyphs
glyphs: Table(u64, Glyph)
metrics: Table(u64, Font_Metrics)
atlas: Atlas
scale: float
device pixels per point
Mouse_Buttonenumexperimentalsince 0.31.0Source
:: enum

Mouse buttons.

LEFT
RIGHT
MIDDLE
button := Mouse_Button.RIGHT;
assert(button != .LEFT);
↗
Modifiersstructexperimentalsince 0.31.0Source
:: struct

Which modifier keys are held. primary is Cmd on macOS and Ctrl elsewhere.

shift: bool
ctrl: bool
alt: bool
super: bool
Cmd on macOS, the Windows key elsewhere
mods := Modifiers.{ ctrl = true };
assert(mods.ctrl && !mods.shift);
↗
Input_Kindenumexperimentalsince 0.31.0Source
:: enum

What an input event is.

MOUSE_MOVE
MOUSE_DOWN
MOUSE_UP
SCROLL
KEY
a key press or auto-repeat, with the key in key
TEXT
typed text, as one code point in character
kind := Input_Kind.KEY;
assert(kind != .TEXT);
↗
Input_Eventstructexperimentalsince 0.31.0Source
:: struct

A queued input event. Keys use the Input module's Key_Code (letters are their upper case ASCII).

x: float
y: float
button: Mouse_Button
repeat: bool
character: u32
mods: Modifiers
dx: float
scroll amount in points
dy: float
Input_Statestructexperimentalsince 0.31.0Source
:: struct

The pointer, modifiers and the keyboard events a frame sees.

events: [..] Input_Event
queued, not yet applied
keys: [..] Input_Event
this frame's KEY and TEXT events, for the focused widget
mouse_x: float
mouse_y: float
mouse_down: bool
drag_start_x: float
drag_start_y: float
mods: Modifiers
clicked: u64
the node a click ended on this frame
double_clicked: u64
scroll_dx: float
scrolling this frame that no scroll container took
scroll_dy: float
cursor: Cursor_Kind
Node_Kindenumexperimentalsince 0.31.0Source
:: enum

How a node lays out and paints.

CONTAINER
lays out children
TEXT
one line of text
LEAF
a size and a custom paint
Paint_Proctypeexperimentalsince 0.31.0Source
:: #type (ui: *UI_State, node: *Node)

A procedure that paints a node's rectangle. It may use ui.draw freely.

Nodestructexperimentalsince 0.31.0Source
:: struct

One widget in the tree. App code reads it through widget's result and the test helpers; the library fills it in.

id: u64
parent: *Node
children: [..] *Node
seen: int
the frame that last declared this node
kind: Node_Kind
Declared every frame.
role: Role
axis: Axis
style: Style
text: string
text_style: Text_Style
label: string
names the widget to tests and a later accessibility export
value: string
the widget's current value, for the same
focusable: bool
key_name: string
the text of the key call that named this node this frame, for warnings (debug only)
pointer: bool
takes mouse input (hover, press)
scrolls: bool
a scroll container
paint: Paint_Proc
paint_data: *void
valid while the frame's build runs and until the next frame
paint_size: Vector2
natural size of a LEAF
natural: Vector2
Computed by layout.
rect: Rect
clip: Rect
the visible part, after the clips of ancestors
content: Vector2
size of a scroll container's content
scroll_x: float
Kept between frames.
scroll_y: float
warned_zero_size: bool
a zero-size focusable has been reported
state: *void
see state
state_free: (data: *void)
state_size: int
repeats: Table(u64, int)
call sites seen under this node this frame
Node_Eventsstructexperimentalsince 0.31.0Source
:: struct

What happened to a node this frame, from the input events routed to it.

hovered: bool
pressed: bool
the primary button is down on it
clicked: bool
the primary button went down and up on it
double_clicked: bool
focused: bool
focus_gained: bool
focus_lost: bool
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);
↗
UI_Statestructexperimentalsince 0.31.0Source
:: struct

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.

theme: Theme
root: *Node
nodes: Table(u64, *Node)
stack: [..] *Node
frame: int
size: Vector2
logical window size
frame_pool: Flat_Pool
key_pending: bool
key_value: u64
key_name: string
draw: Draw_List
hover: u64
Interaction.
pressed: u64
focus: u64
focus_previous: u64
pressed_button: Mouse_Button
click_count: int
last_click_time: float64
last_click_x: float
last_click_y: float
now: float64
seconds, set by the platform loop
wants_frame: bool
Asks for another frame even without input (animation, blinking caret).
wake_time: float64
the time of the next frame that is wanted without input (0: none)
quit: bool
hooks: UI_Hooks
outer: *UI_State
the UI that was current when this one was pushed
ui := headless(.{ width = 640, height = 480 });
defer free_ui(*ui);
assert(ui.size.x == 640);
↗
UI_Hooksstructexperimentalsince 0.31.0Source
:: struct

Platform services the library needs but does not own, set by run_app. Tests leave them null.

clipboard_get: () -> string
clipboard_set: (text: string)
set_cursor: (kind: Cursor_Kind)
ui := headless();
defer free_ui(*ui);
ui.hooks.clipboard_get = () -> string { return "from the hook"; };
assert(ui.hooks.clipboard_get() == "from the hook");
↗
Cursor_Kindenumexperimentalsince 0.31.0Source
:: enum

The mouse cursor shapes widgets ask for.

ARROW
TEXT
RESIZE_HORIZONTAL
RESIZE_VERTICAL
HAND
Draw_Kindenumexperimentalsince 0.31.0Source
:: enum

What a display list entry draws.

RECT
a filled, optionally rounded and bordered rectangle
GLYPH
a quad sampling the glyph atlas, tinted with color
CLIP
sets the clip rectangle for what follows
ui := headless();
defer free_ui(*ui);
draw_rect(*ui, .{ 0, 0, 10, 10 }, rgb(255, 0, 0));
assert(ui.draw.cmds[0].kind == .RECT);
↗
Draw_Cmdstructexperimentalsince 0.31.0Source
:: struct

One display list entry.

kind: Draw_Kind
rect: Rect
color: Color
border_color: Color
radius: float
border: float
uv: Vector4
u0, v0, u1, v1 for a glyph
ui := headless();
defer free_ui(*ui);
draw_rect(*ui, .{ 5, 5, 20, 10 }, rgb(0, 255, 0), radius = 4);
cmd := ui.draw.cmds[0];
assert(cmd.rect.w == 20 && cmd.radius == 4);
↗
Draw_Liststructexperimentalsince 0.31.0Source
:: struct

The display list of a frame plus the clip stack used while building it.

cmds: [..] Draw_Cmd
clips: [..] Rect
ui := headless();
defer free_ui(*ui);
assert(ui.draw.cmds.count == 0);
↗
Containerstructexperimentalsince 0.31.0Source
:: struct

An open container, returned by row, column and the other containers so for can close it.

node: *Node
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
    for row(.{ gap = 8 }) { label("a"); label("b"); }
});
assert(find_label(*ui, "b") != null);
↗
Text_Optionsstructexperimentalsince 0.31.0Source
:: struct

Options for text: all optional.

size: float
points; 0 is the theme's size
color: Color
alpha 0 is the theme's text colour
font: Font_Slot
grow: float
muted: bool
use the theme's muted text colour
padding: float
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
    label("big and quiet", .{ size = 24, muted = true });
});
assert(find_role(*ui, .TEXT).text_style.size == 24);
↗
Button_Variantenumexperimentalsince 0.31.0Source
:: enum

Looks of a button.

NORMAL
PRIMARY
the accent colour
SUBTLE
no background until hovered
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
    button("Save", .{ variant = .PRIMARY });
    button("Cancel", .{ variant = .SUBTLE });
});
↗
Tab_Resultenumexperimentalsince 0.31.0Source
:: enum

What a click on a tab did.

NONE
SELECT
the tab was clicked
CLOSE
its close button was clicked
result := Tab_Result.NONE;
assert(result != .SELECT && result != .CLOSE);
↗
Tab_Optionsstructexperimentalsince 0.31.0Source
:: struct

Options for tab.

dirty: bool
unsaved changes: shown as a dot
closable: bool
ui := headless();
defer free_ui(*ui);
frame(*ui, *ui, (u: *UI_State) {
    for tab_bar() { tab("main.jai", true, .{ dirty = true, closable = true }); }
});
↗
Splitstructexperimentalsince 0.31.0Source
:: struct

An open split: the two panes are visited with for pane: split(...).

node: *Node
position: *float
axis: Axis
min_size: float
width := 150.0;
ui := headless(.{ width = 400, height = 200 });
defer free_ui(*ui);
frame(*ui, *width, (w: *float) {
    for pane: split(.HORIZONTAL, w) {
        if pane == 0 label("left pane");
        else label("right pane");
    }
});
assert(find_label(*ui, "left pane") != null);
↗
Split_Optionsstructexperimentalsince 0.31.0Source
:: struct

Options for split.

min: float
the smallest the first pane may be
max: float
the largest the first pane may be (0: no limit)
options := Split_Options.{ min = 100, max = 300 };
assert(options.min == 100);
↗
Text_Editstructexperimentalsince 0.31.0Source
:: struct

A piece of text being edited. Zero is empty; release it with free_text_edit.

buffer: Text_Buffer
cursor: int
byte offset of the caret
anchor: int
the other end of the selection (equal to cursor when nothing is selected)
goal_x: float
x position vertical movement tries to keep; -1 when not set
scroll_x: float
how far the view is scrolled, in points
scroll_y: float
single_line: bool
Enter is "submit" and line breaks are not accepted
dragging: bool
a mouse drag is selecting
follow_version: int
the buffer version and caret the view last scrolled to show
follow_cursor: int
caret_time: float64
the last time the caret moved, so it is solid while moving
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");
↗
Highlight_Spanstructexperimentalsince 0.31.0Source
:: struct

A coloured range of one line, produced by an editor's highlight procedure.

start: int
byte offsets within the line
end: int
color: Color
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.

Highlight_Proctypeexperimentalsince 0.31.0Source
:: #type (line: string, spans: *[..] Highlight_Span)

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.

Text_Markstructexperimentalsince 0.31.0Source
:: struct

A range of the text drawn with a background: search results, diagnostics, matching brackets.

start: int
byte offsets in the whole text
end: int
color: Color
underline: bool
a wavy-style underline instead of a background
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.

Editor_Optionsstructexperimentalsince 0.31.0Source
:: struct

Options for text_editor and text_field.

font: Font_Slot
size: float
0: the theme's code size
line_numbers: bool
highlight: Highlight_Proc
marks: [] Text_Mark
read_only: bool
placeholder: string
shown in an empty field
grow: float
tab_width: int
options := Editor_Options.{ line_numbers = true, tab_width = 2 };
↗

This one is only checked to compile, not run.

Unitenumexperimentalsince 0.31.0Source
:: enum

What a movement or deletion covers.

CHARACTER
one grapheme cluster
WORD
LINE
to the start or end of the line
DOCUMENT
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);
↗
Key_Resultstructexperimentalsince 0.31.0Source
:: struct

What handle_key did.

changed: bool
the text changed
submitted: bool
Enter in a single-line edit
handled: bool
the key meant something to the editor
result := Key_Result.{ changed = true };
assert(result.changed && !result.submitted);
↗
Headless_Optionsstructexperimentalsince 0.31.0Source
:: struct

Options for headless.

width: float
height: float
scale: float
system_fonts: bool
load the platform's fonts instead of fixed-width synthetic ones
theme: Theme_Mode
options := Headless_Options.{ width = 1024, height = 768, scale = 2 };
ui := headless(options);
defer free_ui(*ui);
assert(ui.size.x == 1024);
↗
Find_Querystructexperimentalsince 0.31.0Source
:: struct

How find picks a node: every field that is set must match.

label: string
the node's label
text: string
the text it draws
role: Role
match_role: bool
role counts
index: int
the nth match, from 0
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(*ui, .{ role = .BUTTON, match_role = true });
assert(node != null && node.label == "Add");
assert(find(*ui, .{ text = "no such text" }) == null);
↗
Rendererstructexperimentalsince 0.31.0Source
:: struct

A renderer bound to one window or to an offscreen target.

instance: WGPUInstance
adapter: WGPUAdapter
device: WGPUDevice
queue: WGPUQueue
surface: WGPUSurface
null when rendering offscreen
uniforms: WGPUBuffer
instances: WGPUBuffer
instance_capacity: int
bind_group: WGPUBindGroup
atlas_view: WGPUTextureView
sampler: WGPUSampler
atlas_version: int
width: int
target size in pixels
height: int
target: WGPUTexture
offscreen only
target_view: WGPUTextureView
staged: [..] Instance_Data
batches: [..] Batch
Instance_Datastructexperimentalsince 0.31.0Source
:: struct

One entry of the instance buffer, as the shader reads it.

rect: Vector4
x, y, width, height in pixels
color: Vector4
border_color: Vector4
params: Vector4
radius, border width, 1 for a glyph, unused
Batchstructexperimentalsince 0.31.0Source
:: struct

A run of instances drawn with one scissor rectangle.

first: int
count: int
x: int
y: int
w: int
h: int
Rendered_Imagestructexperimentalsince 0.31.0Source
:: struct

Pixels read back from an offscreen render: rows top to bottom, 4 bytes per pixel (R, G, B, A).

width: int
height: int
pixels: [..] u8
App_Optionsstructexperimentalsince 0.31.0Source
:: struct

Options for run_app.

title: string
width: int
logical size in points
height: int
theme: Theme_Mode
fonts: bool
load the platform's fonts
frame_limit: int
stop after this many frames (0: run until the window closes); for smoke tests
options := App_Options.{ title = "Notes", width = 900, height = 600 };
↗

This one is only checked to compile, not run.

Constants#

Colorexperimentalsince 0.31.0Source

A colour with straight (not premultiplied) alpha, each channel 0 to 1.

red: Color = .{ 1, 0, 0, 1 };
assert(red == rgb(255, 0, 0));
↗