ui
@profile(t2);
import ui from "ui";
import gfx from "gfx";
component Swatch {
@tape { ui.drawable(); }
prop bg: color = #3366CC;
fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64) {
s.fill_rect(x, y, w, h, bg);
}
}
pub fn main() -> i32 {
ui.run(Swatch {}, 200, 200, "Swatch") or { return 1; };
return 0;
}ui runs the component model: it updates the
tree when state changes, lays it out, paints it on a
gfx.Surface, and delivers pointer and keyboard input.
Showing a tree
| Function | Does |
|---|---|
run(root, w, h, title) -> void or UiError | Open a window and run until it is closed |
run_window(root, Window { ... }) -> void or UiError | The same, with the window placed, frameless or on top |
quit() | End run after the event being handled, as if the window were closed |
headless(root, w, h) -> Screen or UiError | The same tree offscreen, rendered into its own surface |
run_window takes a ui.Window; every field has a default:
| Field | Default | |
|---|---|---|
width, height | 800, 600 | The client area, in pixels |
title | "tape" | UTF-8; any character the font can show |
x, y | null | The window’s outer top-left on the screen; null lets the platform choose (a frameless window is centered) |
frameless | false | No title bar or border: the client area is the whole window |
topmost | false | Stays above other windows |
ui.run_window(Deck {}, ui.Window {
width: 1280; height: 720; title: "Deck";
x: 0; y: 0; frameless: true; topmost: true;
}) or { return 1; };Call ui.quit() from a handler (a key, a click, a timer) to close the
window and return from run. On Linux the placement, frame and stacking
are requests the window manager may ignore; on Uusi the compositor places
windows itself.
Waking on your own handles
A window that also follows something else (a log, a timer, a pipe, a
network connection) names what run should wait on besides the window,
and a function to call when it can be read:
var g_view: *u8 = null;
fn on_log(h: i64) {
let n: i64 = read_some(h); // read what is there now
append_lines(n);
ui.invalidate(g_view); // the view reads module state
}
pub fn main() -> i32 {
let v = LogView {};
g_view = v as *u8;
ui.watch(log_handle, on_log) or { return 1; };
ui.run(v, 640, 400, "Log") or { return 2; };
return 0;
}| Function | Does |
|---|---|
watch(handle, on_ready: fn(i64)) -> void or UiError | Call on_ready(handle) whenever the handle can be read |
unwatch(handle) | Stop |
watch_socket(handle, want, on_ready) -> void or UiError | The same for a net socket (stream.h, listener.h); want is net.READABLE, net.WRITABLE or both |
unwatch_socket(handle) | Stop |
watch_count() | Handles and sockets watched (at most ui.MAX_WATCHES, 15; one more is UiError.TooManyWatches) |
A handle is the platform’s: a Win32 HANDLE (an event, a waitable timer,
a process), a Linux or macOS file descriptor, a Uusi kernel handle (a
channel, devices/log, devices/timer). run waits on the window and
the handles together, so nothing polls: the callback runs as soon as one
is ready, then the window repaints what the callback changed, as after a
click. Sockets are checked every ui.POLL_MS (10 ms) while any is
watched; on macOS watched descriptors are too.
The callback does the reading. A handle stays ready, and its callback
keeps being called, until it has been read (or reset, for a Win32 event
that resets by hand), so read what is there or unwatch it. Watch before
run or from any handler or callback while it runs; a callback may
unwatch its own handle. Watching a handle again replaces its callback.
Headless, screen.ready(handle) does what run does for a ready handle
(its callback, then a frame) and screen.poll_sockets() what it does
every POLL_MS (the ready sockets’ callbacks, then a frame).
on_ready is a module function. A component’s own method can’t go there:
it needs its instance, which a plain function value doesn’t carry, so
ui.watch(h, tick) inside a component is a compile error. A component
puts a ui.Watch or ui.Timer in its view instead (next section).
Timers and watches in a component
A component that samples something (a clock, a device, a socket) has the
loop call one of its own methods, the way a button calls one: ui.Timer
and ui.Watch are components with an event, and the method is bound to
it. They have no size and paint nothing. The state stays in the
component’s vars, and the children get it as props:
@profile(t2);
import ui from "ui";
import gfx from "gfx";
// A bar as long as `percent`: knows nothing of where it comes from
component Meter {
@tape { ui.drawable(); }
prop percent: i64 = 0;
fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64) {
s.fill_rect(x, y, w, h, #202020);
s.fill_rect(x, y, w * percent / 100, h, #33CC66);
}
}
component Monitor {
var cpu: i64 = 0;
var mem: i64 = 0;
fn sample() { // this Monitor's, once a second
cpu = cpu_percent();
mem = memory_percent();
}
view {
ui.Column {
ui.Timer { every: 1000; on_tick: sample; }
Meter { percent: cpu; }
Meter { percent: mem; }
}
}
}
pub fn main() -> i32 {
ui.run(Monitor {}, 300, 80, "Activity") or { return 1; };
return 0;
}Each tick changes cpu and mem; the frame after it repaints the two
meters and nothing else.
| Component | Props | Event |
|---|---|---|
ui.Timer | every: i64 (milliseconds), repeat: bool = true | on_tick() every every ms (once, with repeat: false) |
ui.Watch | handle: i64, want: u8 = 0 | on_ready(handle: i64) whenever handle can be read |
- A
Timercounts from when it is mounted, and again from wheneveryorrepeatchange;everyof 0 or less never ticks. A tick that comes late (a long handler) is not caught up: the timer goes on from then. - A
Watchwatches while it is mounted, again whenhandleorwantchange; a negativehandlewatches nothing.want: 0is a platform handle asui.watchtakes;net.READABLE/net.WRITABLEmake it a socket asui.watch_sockettakes. It counts inui.watch_count(). - Either stops when it leaves the view (an
ifturning false, its component unmounted). ui.Columnandui.Rowgive them no space. A layout of your own skips them withui.takes_space(child).
Headless, time moves only when you say: screen.advance(ms) moves the
ui clock (ui.clock_ms()) on, and every timer due on the way ticks, each
followed by a frame (it returns the ticks):
let m = Monitor {};
var screen: ui.Screen = ui.headless(m, 300, 80) or { return 1; };
@assert(screen.advance(999) == 0);
@assert(screen.advance(1) == 1); // one second after mounting
@assert(ui.frame_painted() == 2); // the two metersMaking components draw and react
A component opts into what it needs with the dispatch groups:
| Call in the body | Methods it then implements |
|---|---|
@tape { ui.drawable(); } | fn paint(s: *gfx.Surface, x: i64, y: i64, w: i64, h: i64); gets bounds_x/y/w/h |
@tape { ui.layoutable(); } | fn layout(x: i64, y: i64, w: i64, h: i64): place the children (ui.layout_tree on each) |
@tape { ui.interactive(); } | fn handle_pointer(ev: *ui.PointerEvent), fn handle_key(ev: ui.KeyEvent); optionally fn focus_changed(focused: bool), fn handle_paste(text: []const u8) |
@tape { ui.focusable(); } | Nothing required: makes it a Tab stop; optionally fn skip_tab() -> bool, fn keeps_tab() -> bool |
A pointer event goes to the component under the pointer and bubbles to its
parents until one sets ev.handled = true. Only components that draw are
hit: one without paint (a wrapper, a ui.Column) lets the pointer
through to its children. A press makes the component that handled it
capture the pointer and take focus: it gets the Move events and the
release (Up if released over it, Cancel otherwise). Keys go to the
focused component. ev.kind is Down, Up, Cancel, Move, Enter,
Leave or Wheel; ev.modifiers holds the ui.Modifier bits (Shift,
Ctrl, Alt, Meta) held at the event, and ev.clicks counts presses
in a row on one component (2 for a double click): close together, within
ui.DOUBLE_CLICK_MS (500) and ui.DOUBLE_CLICK_PX (4). Key events carry
ui.Key codes and ui.Modifier bits.
A Wheel event carries the scroll as it came: ev.wheel_y (positive
toward the end, the content moving up) and ev.wheel_x (positive to the
right), in 1/120 of a wheel’s notch (ui.WHEEL_NOTCH): a touchpad sends a
few units at a time. ev.delta is whole notches, positive away from the
user, with the rest of a notch carried to the next event. ui.ScrollArea
follows the fine amount, both ways.
The clipboard
fn handle_key(ev: ui.KeyEvent) {
if (ui.is_copy_key(ev)) { ui.set_clipboard(selected_text()); }
if (ui.is_paste_key(ev)) { ui.paste(); } // handle_paste gets the text
}
fn handle_paste(text: []const u8) { insert(text); }ev.key is the key as a ui.Key (ev.key == ui.Key.Escape, ui.Key.A,
ui.Key.F5), ui.Key.Other for a code the enum does not name; ev.keycode
is the raw code, the same on every platform (ui.key_of(code) maps one).
A typed character (KeyKind.Char) is a Unicode code point in
ev.codepoint, whatever the keyboard layout; widgets.TextInput keeps
its text as UTF-8 and moves its cursor by characters.
ui.set_clipboard(text) puts text on the system’s clipboard (and keeps it
as the program’s own). ui.paste() asks for it: the focused component’s
handle_paste gets it after the current event, or when the system answers.
On Leivos reading the clipboard needs the user’s paste gesture, so call
paste from the paste key’s (or a click’s) handler. ui.is_copy_key,
is_cut_key and is_paste_key tell the usual keys (Ctrl or Command with
C, X, V; Shift+Insert, Shift+Delete). widgets.TextInput copies, cuts and
pastes with them. When the system’s clipboard is empty or holds no text,
a paste gives nothing. Linux and macOS have the program’s own clipboard
only, for now.
Focus
One component at a time has the keyboard focus. When the window opens
with nothing focused, the first focusable component in view order gets
it, as one Tab would, so a window whose only focusable component is its
root handles keys at once; ui.focus from on_mount chooses another
(null: none). A press moves it to the
component that handled the press; a press that nothing handles (an empty
area) takes it away, so two fields never both show a caret.
focus_changed(true) and focus_changed(false) tell the component that
gets and the one that loses it: draw a caret or a focus ring while it is
focused.
component Field {
@tape { ui.drawable(); ui.interactive(); ui.focusable(); }
state focused: bool = false;
fn focus_changed(f: bool) { focused = f; }
// paint draws the caret while focused ...
}| Function | Does |
|---|---|
ui.has_focus(c) | Whether c has the focus |
ui.focus(c) | Give c the focus (null: take it away). Called before the window opens (from on_mount), it takes effect when input starts, instead of the default first Tab stop: an app’s second field, or null for none |
Tab and Shift+Tab move the focus between the ui.focusable() components,
in view order, wrapping round. One whose skip_tab() returns true is
passed by (a disabled field); one whose keeps_tab() returns true gets Tab
in handle_key while it has the focus (an editor that indents). The Char
event a Tab also makes is not delivered when the Tab moved the focus.
widgets.TextInput is focusable and keeps its caret only while focused.
A key goes to the focused component’s handle_key. A component that
doesn’t write a handle_key of its own (a Slider, a Button) passes the
key on to the nearest component above it that does, so clicking a slider
doesn’t stop your window’s shortcuts. A component with a handle_key
takes every key that reaches it.
Layout containers: ui.Column, ui.Row (below), ui.ScrollArea
(content_w, content_h), ui.WindowLayer (overlapping windows,
ui.raise). ui.show_dialog(d) paints d over everything and gives it
the pointer until ui.close_dialog().
Column and Row
ui.Column stacks its children top to bottom, ui.Row left to right.
Each has three props of its own:
| Prop | Default | Does |
|---|---|---|
gap | 0 | Pixels between two children |
padding | 0 | Pixels kept free inside every edge |
align | ui.Align.Stretch | Where a child with a fixed size sits across: Start, Center, End; Stretch fills the width (a Column) or the height (a Row) |
Their children size themselves with three more props, width,
height and grow, which every widget, ui.Column, ui.Row and
ui.ScrollArea have:
width: 120(in a Row) orheight: 32(in a Column) is a fixed size;- the children without one share what is left, in proportion to their
grow(1 unless given):grow: 2takes twice its siblings’ share,grow: 0none; - across, a fixed size is kept and placed by the container’s
align.
ui.Column { padding: 12; gap: 8;
widgets.Label { text: "Name"; height: 20; }
widgets.TextInput { height: 28; }
widgets.Panel {} // grows: the rest of the height
ui.Row { height: 32; gap: 8; align: ui.Align.Center;
widgets.Panel {} // pushes the buttons right
widgets.Button { label: "Cancel"; width: 90; height: 28; }
widgets.Button { label: "Save"; width: 90; height: 28; }
}
}grow: 0 with no fixed size gives a child its own size, the one it
measures: a Button as wide as its label, a Label as tall as its text.
With an align other than Stretch it takes its own size across too.
ui.Column and ui.Row measure as well (their children, gaps and
padding), so a ui.Row { grow: 0; ... } in a Column is as tall as its
tallest child:
ui.Row { grow: 0; gap: 8; padding: 8;
widgets.Button { label: "Open"; grow: 0; }
widgets.Button { label: "Save as..."; grow: 0; }
widgets.Panel {} // the rest
}With none of them given, the children share the space equally, as
before. Your own component takes the three sizing props with
@tape { ui.sizable(); }; a component without them is laid out as one
with grow: 1. It measures itself with @tape { ui.measurable(); } and a
fn measure(max_w: i64, max_h: i64) -> ui.Size; ui.natural_size(child, max_w, max_h) is what a layout asks (fixed sizes first, then the
measure). Being props, all of them can be set by #style rules
(ui.Row { gap: 8; }), so a module’s spacing can live in one place.
A layout of your own reads a child’s through if (child is ui.Sizable) { ... child.width ... }, or hands its children to
ui.layout_line(__self, vertical, x, y, w, h, gap, padding, align), the
one Column and Row use.
Animating
A component that moves with time is @tape { ui.animated(); } with a
fn frame(dt_ms: f64) -> bool: after ui.animate(comp), ui calls it
every frame (16 ms) until it returns false. Step tweens there and assign
what the view reads. ui.stop_animation(comp) stops it, and
screen.advance(ms) runs the frames headless. See
tween and easing.
Keeping a view current
Keep what a view shows in the component’s vars, change it in the
component’s own methods (bound to events: a click, a key, a ui.Watch or
ui.Timer for handles and timers), and hand it down as props:
import widgets from "widgets";
component Clicks {
var count: i64 = 0;
fn bump() { count = count + 1; }
view {
ui.Column {
widgets.Label { text: "clicks: {count}"; }
widgets.Button { label: "+1"; on_click: bump; }
}
}
}A binding (text: "clicks: {count}", size: count % 3, a prop handed on
to a child’s own view) is evaluated again when a var, state or prop
it reads changes, and the child it sets repaints if the value differs.
Nothing else is needed. A var changes when it is assigned, when an
element or field of it is, and when a method that may change it (one
taking *self) is called on it: items.push(x) re-reads
text: "{items.len()}".
For a list prop (a slice), “differs” means its contents: rebuilding a list
in place with the same rows repaints nothing, and only the components
whose rows changed repaint. Numbers, strings, nested lists, floats and
optionals inside the rows all count. A pointer inside a row counts by the
address it holds, not by what it points at: if that data changes, call
ui.invalidate on the component.
ui.invalidate(comp) is the explicit way out, for views that read state
changed outside the tree, such as a module variable a module function
given to ui.watch updates (the sample above). A binding that reads a
module variable or calls a function is evaluated when its instance is
built and again only at ui.invalidate: ui doesn’t follow module
state. invalidate(comp) evaluates every binding and if/for of comp
and of the components under it on the next frame, and repaints comp’s
area.
What gets repainted
A frame repaints only what changed. A component is dirty when one of its
vars or states is assigned (an element or field of one included) or
its parent gives it a new prop value; the frame repaints the dirty
components’ bounds, the old and new bounds of anything layout moved, and
where removed components were: those rectangles (at most
ui.MAX_RECTS, 8) are cleared and every drawable that meets them is
painted again in tree order, clipped to them, and a window presents just
them. A pointer move that changes no state repaints and presents
nothing. So:
paintdraws inside the component’s bounds (gfxdrawing is clipped there; code that writess’s pixels itself keeps insides.clip()) and from the component’s own state, props and bounds;- what
paintorlayoutwrites (a position kept for hit tests) is not a change; - a view or
paintthat reads something ui can’t see (a module variable, a theme switch) needsui.invalidate(comp), which evaluates the view’s bindings again and repaints the component’s whole area (the whole window for the root).
Testing without a window
A Screen from headless works like a window that you drive yourself:
let app = Counter {};
var screen: ui.Screen = ui.headless(app, 200, 100) or { return 1; };
screen.click(20, 50); // press and release
screen.type_text("hello"); // keys for the focused component
screen.press(ui.Key.Return);
@assert(screen.pixel(5, 5) == #FFFFFF);
@assert(screen.checksum() == 1234567890); // the whole picture
let r: gfx.Rect = ui.bounds(ui.child(app as *u8, 0)); // where layout put a child
screen.free();| Method | Does |
|---|---|
pointer(kind, x, y), click(x, y), wheel(x, y, delta), scroll(x, y, dx, dy) | Pointer input (scroll: a touchpad’s amounts, 1/120 notch), then a new frame |
hold(modifiers) | The ui.Modifier bits held for the pointer events that follow (a key event sets them too) |
key(kind, keycode, codepoint, modifiers), press(key), type_text(text) | Keyboard input, then a new frame |
pixel(x, y), checksum(), focus() | What the last frame shows; who has focus |
frame() | Render again (after changing state yourself) |
ready(handle), poll_sockets() | A watched handle’s callback, or the ready sockets’, then a new frame |
advance(ms) | The ui clock moved on: each ui.Timer due ticks, then a new frame |
free() | Free the screen’s surface; the component tree stays (ui.unmount it) |
What the last frame (of a screen or a window) did, for tests that count repaints:
| Function | Is |
|---|---|
ui.frame_painted() | Components it painted (0 when nothing had changed) |
ui.frame_damage(), ui.frame_rect(i) | The rectangles it repainted (0 to 8) |
ui.paint_count() | Components painted since the program started |
screen.pointer(ui.PointerKind.Move, 5, 5); // into a button
@assert(ui.frame_painted() == 1); // the button, nothing else
screen.pointer(ui.PointerKind.Move, 6, 5); // still in it
@assert(ui.frame_damage() == 0); // nothing repaintedNo display is needed, so these tests run anywhere tape test does.
The tree
| Function | Does |
|---|---|
child(comp, i), child_total(comp), children(comp) | The children, through if and for |
parent(comp) | The parent component |
bounds(comp) -> gfx.Rect | Where the last layout put a drawable component |
invalidate(comp) | Re-evaluate every binding and if/for of it and the components under it (module variables and calls included), and repaint its area, on the next frame |
hit_test(root, x, y) | The component under a point |
unmount(comp) | Destroy comp and everything under it: on_unmount, then its children, then its memory (see Lifecycle) |
ui.update_tree, layout_tree, paint_tree, do_pointer and do_key
are the passes run and Screen use, for frameworks of your own.
Last modified: