# 0039: libWolkit and Wolkit

> A Rust engine draws the window; a Crystal shard owns views, signals and the tools; patches and events cross between them as data.

Source: https://wolos.org/reference/adr-0039

## Context

0036 said wolkit is a declarative view tree that maps one to one onto `shared::view`, drawn by the Kit host, with "a small 2D canvas over Vulkan" for acceleration and a `wol_surface` family in `wol.h`. Three limits make that too small for the lead's goal of a fast, reactive, GPU-first UI kit. The view wire carries at most 200 parts, 12 levels and ten kinds, with no custom styling, no long lists and no canvas. The whole tree crosses as one JSON line on every change. And everything the kit can do is whatever the GPL Kit host already draws, so an app cannot be faster than the host. The lead's two rulings are a Rust library with a stable C ABI that holds the engine, and a thin Crystal binding that owns the DSL, the runtime abstractions and the developer experience.

## Decision

- **libWolkit is the engine.** A new crate `crates/wolkit` (package `wolkit`, `libwolkit.so.1`, header `crates/wolkit/include/wolkit.h`), BSD-2-Clause, depending only on `shared` and `core`. It holds the retained scene, style, layout, text, paint, a CPU reference rasteriser and a Vulkan renderer, input routing, the accessibility tree and the animation clock. Its C ABI has 53 verbs, opaque handles, UTF-8 with lengths, a status code and an optional error sentence, one thread at a time per engine, and no callbacks.
- **Everything else crosses as data.** An app changes a window with one binary patch stream per frame (`wolkit_window_apply`, format `WKP1`, interface I10) and learns what happened by draining binary event records (`wolkit_engine_read_events`, interface I12). Ids of node kinds, properties, ops, events, roles and state bits are append-only, listed in the header and snapshot-tested (`abi/wolkit-1.0.props`, `abi/wolkit-1.0.h`). A newer app can send an optional op to an older engine because every op is length-prefixed with a must-understand flag. New widget behaviour adds ids, not symbols.
- **One scene ABI, two backends per window.** The *kit backend* lowers the scene to `shared::view` and talks to the Kit host over its existing socket, so it needs no change to the GPL side and ships first. The *native backend* is the engine's own window and becomes the default after a parity gate (screen reader, input method, clipboard and drag and drop, budgets, look parity, fuzzing, audit). The kit backend stays supported for at least two releases after that. A *headless backend* has no display and backs the conformance suite, the golden harness and the shard's test harness. One conformance suite (`crates/wolkit/tests/conformance.rs`) runs on every backend with a per-backend list of expected differences.
- **The shard owns the DSL and the user-level reactive layer.** `Signal`, `computed`, `effect`, `Resource`, components, navigation and the developer tools stay in Crystal because the values and closures live in the Crystal heap. The engine owns a render-level dirty graph (per-node dirty bits for style, layout, paint and accessibility) and the animation clock, so springs and tweens run in Rust. Cost, stated plainly: a binding for another language must write its own user-level reactive layer. 0036 already says WolOS writes no high layer for other languages.
- **0036 amended in two places.** wolkit is no longer "a view-tree builder mapping one to one onto `shared::view`": `shared::view` is the kit backend's wire only, and the shard's golden is a decoded scene dump, not view JSON. The GPU paragraph's "small 2D canvas over Vulkan" becomes the engine's own `canvas` and `surface` nodes (`wolkit_window_canvas_submit`, `wolkit_surface_*`); the `wol_surface` family is not added to `wol.h` (it stays 1.14), because native windows are not Kit-host windows. If the lead keeps `wol_surface` in `wol.h` for Kit-host windows, it is added as 1.15 independent of this engine.
- **Boundaries.** `crates/wolkit` may depend on `shared` and `core` and on permissive third-party crates allowed by `deny.toml`; no GPL crate depends on it (`crates/shared/tests/architecture.rs`). `unsafe` is confined to `src/ffi/` (the C boundary), `src/loop_/` (one `poll` call), `src/gpu/` and `src/window/native/`, each block with a `// SAFETY:` comment, listed in `RR-068` (`docs/RULES.md`, `crates/shared/tests/rules_rust.rs`) and counted by `crates/shared/tests/unsafe_ratchet.rs` (a `wolkit` row, provisional at creation and lowered by the closing worker). Raw slices from C pointers are built only in `src/ffi/mod.rs` (`bytes_in`, `bytes_out`, behind `clippy.toml`'s disallowed methods). Every list is bounded (`docs/modules/libwolkit.md`, Limits). Untrusted input is validated before it is applied and applied atomically; every decoder has a fuzz target (`wolkit_patch`, `wolkit_wsb`, `wolkit_wolcss`). A panic stops at the boundary as `WOLKIT_ERR_INTERNAL`.
- **Crystal rules this adds** (next free `RCR` ids, checked by `crates/controller/tests/rules_crystal.rs`): `FFI_ALLOW` gains the wolkit FFI files, and `ALLOWED_SHARDS` and the shard census gain `cryswol` and `wolkit`, so an app that requires `wolkit` passes RCR-043.

## Consequences

the engine is a second large Rust component beside the Kit host, about 45 to 60 thousand lines by the plan's estimate (not measured), with its own text, layout and rendering to keep correct and fast. In return an app is no longer limited by the Kit's 200 parts, can scroll a hundred thousand rows, can draw, and can share a GPU device with the AI framework (`wolkit_engine_get_gpu_uuid`). The look of native windows must track the Kit's: both read the same token sources (`design/tokens/colors.json`, `crates/shared/src/theme.rs`, `crates/shell/src/kit/contrast.css`) and a drift test compares them. Two open items sit outside the engine: a compositor rule so a toplevel can ask for glass, and a hook so native windows reach the global menu bar (`crates/shell/src/appmenu.rs`); until they land, `backdrop: glass` draws the normal translucent token colour and a native window carries its own menu. The version of the ABI is 1.0 until the first release that ships it; after that a shipped symbol never changes.
