Skip to content

sparkles:ui — Library-wide Feature Requirements

Status: living inventory · Date: 2026-08-05 · Scope: requirements common to the whole toolkit — the architecture (UIA), the package graph and its build constraints (PKG), and non-functional properties (NFR).

Architecture (UIA)

IDRequirementStatusTraces to
UIA1The toolkit must be canvas-first: the library owns semantic behavior and widget composition, while target adapters provide drawing, native-input translation, device measurement and declared capabilities. No native OS widget may replace a toolkit widget on any target.fullcanvas.d isCanvas; ui-raylib; ui-tui
UIA2It must be structured in three levels — state machines (STM), layout (LAY), widgets (WGT) — each usable independently, each lower level free of presentation.partialstate.d; layout.d; widget.d
UIA3The backend contract must be a capability concept (isCanvas!T, checked structurally with __traits(compiles)), not an interface or class hierarchy — so attributes infer, there is no vtable cost, and a backend need not inherit anything.fullcanvas.d isCanvas
UIA4The pipeline must be view() → layout() → buildDisplayList() → paint(canvas), with every stage before paint @safe and free of GPU/terminal state, so the whole toolkit is testable through a recording canvas with no window and no tty.fulllayout.d; display_list.d; interp/immediate.d; canvas.d RecordingCanvas
UIA5A widget must name a semantic slot, never a concrete color. Appearance is resolved from the theme during display-list construction.fullstyle.d Slot; display_list.d resolveVisual
UIA6The same widget definition must render on every target unchanged; only the canvas and the interpreter differ. Where a target cannot honour a feature, it must degrade through a declared capability set, not silently.partialbackends.md TGT5
UIA7The toolkit must be presentation-complete for a real application — chrome (headers, status bars, scrollbars, gutters, inputs, toasts), content (rich text, tables, trees, lists), and containers (panels, popups, scroll views) — so a consumer needs no per-backend rendering of its own.not startedwidgets.md WGT7+

Package graph (PKG)

IDRequirementStatusTraces to
PKG1Backends adapt to sparkles:ui, never the reverse. libs/ui must not depend on any backend package (raylib-text, tui, a GPU library) nor on core-cli.fulllibs/ui/dub.sdl
PKG2sparkles:ui depends only on sparkles:base, sparkles:input, sparkles:math and expected (the last two both already base's own). Concrete canvases live in sibling packages — sparkles:ui-tui, sparkles:ui-raylib — so every consumer picks the backends it wants. Every dependency must be declared, not inherited through another package's import paths.not startedproposed libs/ui-tui, libs/ui-raylib
PKG3Terminal capability probing belongs to sparkles:base (it is an environment query, not a UI concern); the terminal's cell-geometry types belong to sparkles:tui. Neither may pull core-cli into a UI dependency chain.full (31fb39c5)base.term_caps; tui.geometry
PKG4sparkles:core-cli is scoped to CLI concerns — argument parsing, help, prompts, process utilities. Its UI components move into sparkles:ui; its help output is expressed as widgets.partialmigration.md MIG2 (components moved; help still string-based)
PKG5Packages that core-cli depends on must take the cycle-safe integration pathsparkles:math on importPaths rather than as a dependency, and the test-runner shim/impl source-included rather than depended upon. This applies to ui and input once core-cli depends on them.not startedlibs/ui/dub.sdl; libs/input/dub.sdl
PKG6sparkles:ui is a library, not a sourceLibrary — its component set is too large to recompile inside every consumer.not startedlibs/ui/dub.sdl

IMPORTANT

PKG5 is not hypothetical. Dub unions dependencies across configurations, so once core-cli depends on sparkles:ui (which PKG4 requires, for help output), a real sparkles:math dependency closes core-cli → ui → math → (math's unittest) → test-runner → impl → core-cli, and ui's own dependency "sparkles:test-runner" closes ui → test-runner → impl → core-cli → ui. sparkles:core-cli already documents and solves the first by making ../math/src import-only, and base/core-cli/test-utils solve the second by source-including the runner. ui and input need both.

Note the pressure does not come from the test runner's own use of these components: it reaches them by introspection (__traits(compiles, …)), never by dependency — precisely because base and core-cli source-include the runner when testing themselves. That guard must survive the move (MIG6).

Target graph

base          → expected                        (+ terminal capability probing)
math          → (base, test-runner in unittest)
input         → base, math (import-only)
ui            → base, input, expected, math (import-only)
ui-tui        → ui, tui
ui-raylib     → ui, raylib-text
tui           → base, input, math (import-only)
raylib-text   → base, raylib-d
syntax        → base, ui, tree-sitter
core-cli      → base, ui, expected

Non-functional (NFR)

IDRequirementStatusTraces to
NFR1Layout and display-list construction must be @safe pure nothrow and run in O(n) over the node count.partiallayout.d; display_list.d
NFR2The widget arena, display list and per-frame scratch buffers must have a @nogc path via SmallBuffer, so a per-frame rebuild allocates nothing steady-state. The node and operation types are chosen so this swap is non-breaking.partial (eea336c3) — the display list has the path (buildDisplayListInto, @nogc asserted at compile time); the widget arena does not, and the retained consumers have not taken it (UI-O4)display_list.d buildDisplayListInto; widget.d
NFR3Every stage before paint must be exercisable with no GPU context and no terminal, through the recording canvas.fullcanvas.d RecordingCanvas
NFR4A full-screen relayout plus display-list construction must complete in under 1 ms for a tree of 2 000 nodes (a full terminal of decorated content), measured by a @benchmark test. Incremental relayout is deferred until that budget is actually missed.researchedlayout.md LAY13
NFR5The toolkit must carry a parity harness: the same widget tree rendered through every backend, with the HTML target usable as a browser ground-truth oracle, and theme values asserted in lockstep against the stylesheet they mirror.partialinterp/html.d; twoslash CSS lockstep tests

IMPORTANT

NFR2 has a prerequisite outside this package, and it is the reason the row has stayed "not started" rather than being a small mechanical swap. A DrawOp contains a borrowed text slice, and SmallBuffer cannot safely hold an element type with references today: its inline slots are void-initialized, and its heap block is allocated outside the GC's scanned memory, so a buffer that outgrows its inline storage can have the text its operations point at collected. This is why RecordingCanvas uses a GC array and says so in its documentation.

That prerequisite is now met (350ba75d): SmallBuffer registers its heap block as a GC root and initializes its inline slots when the element type carries references, so a DrawOp buffer is safe. What remains is not the allocation but the ownership it changes — see UI-O4.

Module coverage

Source fileRequirements
libs/ui/dub.sdlPKG1, PKG2, PKG5, PKG6
libs/ui/src/sparkles/ui/package.dUIA2, UIA4
libs/ui/src/sparkles/ui/canvas.dUIA1, UIA3, NFR3
libs/ui/src/sparkles/ui/layout.dUIA4, NFR1
libs/ui/src/sparkles/ui/display_list.dUIA4, UIA5, NFR1, NFR2
libs/ui/src/sparkles/ui/widget.dUIA2, NFR2
libs/ui/src/sparkles/ui/interp/html.dUIA6, NFR5

Relationship to existing specs

PieceRole
principles.md PRNthe architectural rules these requirements operationalise
backends.md TGTthe concrete targets satisfying UIA1/UIA3/UIA6
migration.md MIGhow PKG3/PKG4 are executed without breaking consumers
hue UI architecturehue's consumption of this toolkit (this spec supersedes its library-level requirements)
AGENTS.mdthe monorepo's package table and test-runner integration recipes

Overview · Principles · Layout · Backends