WolOS
Reference contents

Reference · Apps and the UI kit

libWolkit: the UI engine

The engine under Wolkit: scene, style, layout, text, paint, input and accessibility, behind a small versioned C interface.

On this page

What it is

libWolkit is the engine that draws Wolkit windows. An app sends it patches and reads back events; there are no callbacks and no call per part.

Why

Keeping the scene in the engine means the app's own memory stays small and a frame costs one call per window. A versioned C interface lets apps in any language use it.

How

Most apps never call it directly: the Wolkit shard does. This page is the reference for the wire formats, the limits and the backends.

Wolkit reference · Wolkit benchmarks · Decision: libWolkit and Wolkit

Reference

One engine for every WolOS app that draws its own window: wolkit.h + libwolkit.so.1. Code: crates/wolkit (Rust, BSD-2-Clause, depends on shared and core only). The Crystal binding is the wolkit shard (docs/modules/wolkit.md); the style language is docs/modules/wolkit-style.md. Amends ADR 0036.

An app builds a scene with patch streams (wolkit_window_apply) and hears back through event records (wolkit_engine_read_events). There are no callbacks and no per-node calls.

Rules (the header comment has the full text)

RuleMeans
Opaque handles_create returns a reference to _release; _retain adds one; _get_ borrows; _release(NULL) is fine; a wrong or released handle is refused, not used (src/ffi/handle.rs)
Strings and buffersUTF-8 plus length; caller-owned, borrowed for the call; the engine copies what it keeps
Errorswolkit_status return (0 OK, 1 INVALID_ARGUMENT, 2 PERMISSION_DENIED, 3 UNAVAILABLE, 4 INTERNAL, 5 LIMIT, 6 DEVICE_LOST); wolkit_error ** filled only on failure, with one sentence for a person
"needed" buffers*needed is always set; a buffer too small gets WOLKIT_ERR_LIMIT and is left alone
Threadsretain/release anywhere; the rest one thread at a time per engine (a lock makes a mistake a wait)
Event looppoll wolkit_engine_get_fd, call wolkit_engine_dispatch(engine, 0, &flags, &err), drain with wolkit_engine_read_events
Stabilitysymbols versioned by abi/wolkit.map; ids in the header are append-only

The pipeline

patch stream -> scene arena (dirty bits) -> style -> layout -> paint -> CPU raster or Vulkan. src/window/pipeline.rs runs style then layout; each stage reads the scene through SceneRead and writes only what it computed through SceneWrite (src/scene/read.rs). Rectangles are stored in window space, before scroll offsets. A patch marks only STYLE (properties, classes, state) or LAYOUT|PAINT (text, structure); the style stage derives the rest by comparing computed styles.

PartPathOwner
Scene arena, patch decoder and writer, atomic applysrc/scene/W1
Event records and queuesrc/event/W1
Engine, windows, dispatchsrc/engine.rs, src/window/W1
C ABIsrc/ffi/W1
Style, layoutsrc/style/, src/layout/W2
Text, paint, CPU rasterisersrc/text/, src/paint/, src/cpu/W3
Native window, input, accessibilitysrc/window/native/, src/input/, src/a11y/W4
GPUsrc/gpu/W5

Patch stream v1 (interface I10)

Little-endian. Header (24 bytes): u32 magic "WKP1", u16 format (1), u16 0, u32 seq, u32 op_count, u32 pool_len, u32 0. Then the ops, then the string pool (the last pool_len bytes). A patch is a multiple of 4 bytes and at most 1 MiB.

An op is u16 opcode, u16 (bit 15 must-understand | length in 4-byte words, header included), then payload words. An unknown opcode without the flag is skipped; with it the whole patch is refused. A known op has exactly its number of words.

The string pool holds entries of u32 length, the bytes, zero padding to 4. A string reference is the byte offset of an entry; WOLKIT_NO_STR is the empty string. UTF-8 is checked once, over the pool, before anything is applied.

A node id is generation << 24 | slot. The slot is below 65,536; slot 0 is the window itself and always exists. The app chooses ids, so creating a node needs no round trip; a stale id (wrong generation) is refused.

OpWords after the headerMeaning
CREATEnode, kindallocate a node (not yet in the tree)
INSERTparent, child, indexplace a detached node; WOLKIT_APPEND as index appends
REMOVEnodedetach (the node and its subtree stay)
MOVEnode, parent, indexdetach and place
SET_PROPnode, prop, value wordsinline property; words by type (below)
SET_TEXTnode, strreftext of text, button, toggle, text input
SET_STYLEnode, n, class ids (n at most 8)classes from the loaded style table
SET_STATEnode, maskapp-owned state bits (WOLKIT_STATE_DISABLED, SELECTED, CHECKED, EXPANDED, BUSY)
SET_KEYnode, lo, histable key for keyed children
BIND_EVENTnode, maskwhich events this node wants (WOLKIT_BIND_*)
SET_A11Ynode, role, name strref, description strrefaccessibility
ANIMnode, prop, target f32, mode, paramtween or spring on a number property
LIST_RANGEnode, count, row height f32, overscanvirtual list description
FOCUSnoderequest focus (0 clears)
FREEnoderelease the node and its subtree

Property value words by type: f32, i32, bool, enum, token, str, u32 one word; dim two (unit 0 auto, 1 px, 2 percent; then f32); pair two f32; border (width f32, colour token); gradient five (angle f32, count, three tokens); shadow four (dx, dy, blur f32, token); transition three. The table is src/scene/prop.rs; abi/wolkit-1.0.props snapshots it.

Atomic apply. The patch is validated first (Patch::parse: framing, every op's shape and values, the pool, every string reference). Then every change is journaled; the first op that cannot apply rolls the journal back, so the window is exactly as before and the caller gets one sentence. FREE takes effect at the end of the patch for the same reason. A backend that cannot show the result (the Kit wire's 200 parts) refuses the patch the same way.

How to add a property. Append a row to props! in src/scene/prop.rs with the next free id for its group, run tests/abi.rs (it tells you the header and abi/wolkit-1.0.props need the row), add the #define to include/wolkit.h and, if the version is already shipped, a line to a new snapshot (abi/wolkit-1.1.props) instead of editing the shipped one. A style meaning belongs to src/style/, not here.

Event record v1 (interface I12)

u32 len (whole record, a multiple of 8), u16 kind, u16 flags, u32 window, u32 node, u32 seq, then the payload from byte 20 (read it unaligned), zero padded. seq is the sequence number of the last patch the engine had applied: an edit event made before a patch the app has since sent can be told from news. Flag 1 says events were dropped just before this record. Events go to the nearest ancestor (or the node) that bound the kind; window events (resize, close request, theme, frame tick, visibility, diagnostic) always arrive.

KindPayload
CLICKu32 button, u32 mods, f32 x, f32 y
POINTER_ENTER, POINTER_LEAVEf32 x, f32 y
KEYu32 keysym, u32 pressed, u32 mods, u32 repeat
TEXT_CHANGED, TEXT_SUBMITu32 caret, u32 anchor, u32 length, then the text bytes
TOGGLEu32 on
SLIDERf32 value
SCROLLf32 x, f32 y
FOCUSu32 gained
RESIZEu32 width, u32 height, f32 scale
THEME_CHANGEDu32 dark, u32 accent_rgb, u32 flags
DIAGNOSTICu32 level (0 note, 1 warning, 2 error), u32 length, the text bytes
othersdefined with the part that makes them (drop, drag, list range, surface, a11y action)

The queue holds at most 4,096 records and 4 MiB. A newer scroll, slider, resize, frame tick or hover event replaces the pending one of the same node; when the queue is full the oldest of those is dropped first and the next record carries the overflow flag. A buffer of WOLKIT_EVENT_MAX_RECORD (about 64 KiB) bytes always makes progress.

Backends

BackendWhenNotes
headlessWOLKIT_ENGINE_HEADLESSno display; virtual clock (wolkit_window_advance_time); input injection; wolkit_window_render_rgba; the only one that accepts wolkit_engine_set_environment
kitWOLKIT_ENGINE_FORCE_KIT, or no native backendlowers to shared::view (src/window/kit/lower.rs), one socket per engine ($WOL_KIT_SOCKET), one reader thread wolkit-kit-rx, lines at most 256 KiB, a slow or silent host never blocks the app
nativedefault once it existsthe engine's own window (src/window/native/)

wolkit_window_render_rgba and the native wl_shm path draw the same frame (window::pipeline::render_rgba): the scene lowered to a display list with its computed styles, the token colours of the built-in light or dark theme under the person's accent and contrast (src/paint/palette.rs), and text shaped with real fonts (src/paint/scene.rs), then the CPU rasteriser. Layout measures text with the same shaper through a per-window paragraph cache keyed by text, width and style (src/text/measure.rs), so paint reuses what layout shaped. Fonts are one set per process (the first engine decides it): an engine made through the C ABI indexes the person's font folders (lazily, at the first window), except a headless one (WOLKIT_ENGINE_HEADLESS), which reads only the pinned folder /usr/share/fonts/inter (the UI face the goldens use; WOLKIT_FONT_DIR names another), so its screenshots are the same on every machine that has that folder; an engine made in-process without that (the Rust tests) has no fonts and draws every glyph as the missing-glyph box, which is the same on every machine.

Kit lowering, and what does not survive: box becomes a row or a stack (direction), spacing the nearest of 0, 4, 8, 12, 16, 24; text a label with its role; button, toggle and text_input take their label from their text, else their first text child, else their accessibility name (none is an error); slider, image (size snapped to the Kit's sizes) and vlist (all mounted rows as a list) map directly. Custom style properties, padding, borders, shadows, transforms and animations are ignored. canvas and surface become one placeholder sentence; overlay, divider and hidden parts are omitted. More than 200 parts or 12 levels is WOLKIT_ERR_LIMIT. A part's wire id is its node id, so a click on the wire maps straight back. Golden: tests/kit_golden/kit_app.json (rerun with WOLKIT_BLESS=1 and review the diff).

Dumps

wolkit_window_dump returns text; the shard's golden patch-stream specs compare against it. WOLKIT_DUMP_SCENE: one line per node, indented by depth: id kind, then classes=, state=, key=, on= (the bind mask), each inline property as name=value, then the text in quotes. WOLKIT_DUMP_LAYOUT: id kind x y w h. WOLKIT_DUMP_A11Y: id kind role=… name="…", focused, state=….

Limits (every list bounded)

LimitValue
Nodes per window65,536 (the kit backend: 200)
Tree depth64 (the kit backend: 12)
Windows per engine32
Patch bytes per apply1 MiB
Text per node64 KiB
Classes per node8
Pending events4,096 records, 4 MiB
Decoded image16,384 px a side, 256 megapixels; 512 MB of images
Style table4 MiB

Security and untrusted input

The patch stream, style tables, style sheets, images and fonts are untrusted. Each is bounded, validated before use and fuzzed (fuzz/fuzz_targets/wolkit_patch.rs, wolkit_wsb.rs, wolkit_wolcss.rs; seeds in fuzz/regressions/; crates/wolkit/tests/patch_fuzz.rs replays them and a deterministic mutation on stable). A panic stops at the boundary as WOLKIT_ERR_INTERNAL. The engine opens only the Kit socket (kit backend) and, on the native backend, the Wayland socket; it has no privilege and sends no telemetry.

Tests

cargo test -p wolkit: tests/abi.rs (exports, linker map, header against the Rust signatures and constants, shipped declarations and property ids never change), tests/c_hello.rs (a C program, tests/c/hello.c, builds a window, clicks and reads the event), tests/conformance.rs (the same scenarios on every backend, with the reasons for each difference), tests/alloc_free.rs (zero heap allocations in a steady-state apply and frame, measured with a counting allocator), tests/patch_fuzz.rs. Shared test helpers (the fake Kit host, the libwolkit.so.1 link, the C compile) are in tests/support/. anim/ is the animation clock: ANIM ops run as tweens or springs on the headless virtual clock (wolkit_window_advance_time) and the native frame clock; the Kit host gets the final value at once.

From the developer documentation, wonderlandos/wolos docs/modules/libwolkit.md.