Skip to content

sparkles:ui widgets — Feature Requirements (WGT, VMD)

Status: partial · Date: 2026-08-05 · Scope: the widget level — the tree representation, props and identity, and the component catalog, split into backend-independent view models (VMD) and views (WGT).

Design & rationale

View model + view

The toolkit's components are split in two, and the split is the point:

  • A view model is presentation-free. It owns the component's data and interaction state and answers questions about it. It has no idea how it is drawn, contains no colors or glyphs, and is testable with no canvas at all.
  • A view is a pure function from a view model to a widget subtree. It owns the visual decisions — slots, glyphs, spacing — and nothing else.

This is what makes "same model, different UIs" true rather than aspirational, and it is the pattern the tree-view case study singles out as the central design insight worth copying. That study also names the failure mode precisely: mixing expand state, git status and diagnostic severity onto the data node means the tree can only ever have one visual state, and the whole structure becomes uncopyable.

The tree component is therefore the exemplar every other component follows:

LayerContent
dataflat node arena with index links — an independent structural snapshot is one arena duplication
interactionopened set, selection, scroll offset — keyed by identity, not stored on nodes
viewborrows both, owns glyphs and slots only

with the flatten step — hierarchy to a linear list of visible rows — as a pure free function, not a method that also lazily loads children and applies filters.

Why the tree is not recursive

A recursive node type makes the structure an incidental one: ownership is unclear, copying is a deep traversal, and every consumer writes its own walk. A flat arena with index links can be duplicated in one pass, is cache-friendly and @nogc-able, and lets a single traversal serve every algorithm. The current D slices still alias under default copy; independent value semantics are tracked as UI-O1.

Widget tree (WGT1WGT6)

IDRequirementStatusTraces to
WGT1A widget tree must be a flat arena — one relocatable buffer of nodes, containers referencing children by explicit index list — not a class hierarchy or a recursive value.fullwidget.d Widget, WidgetTree
WGT2A view must be a pure function view(model, ctx) → WidgetTree, with no dependence on frame state, so it is re-entrant: any view may embed the output of another view at any depth.partialwidget.d Builder
WGT3A widget's payload must be a sum type over the widget kinds, so only the fields meaningful for a kind exist, the compiler enforces exhaustive handling, and adding a kind cannot silently skip a backend.not starteddeliberately sequenced after W2–W5: freezing the payload sum before the component catalog exists would mean re-cutting it per component. Exhaustiveness is meanwhile enforced by final switch over WidgetKind.
WGT4Widget props must be Regular with total, substitutive structural equality and copy independence; handlers and other non-comparable payloads must be excluded from the compared value.partialhandlers and element state are excluded (00b331ad), but mutable slice payloads still alias under default copy (UI-O1)
WGT5A widget may carry a key, and the renderer must maintain a store of per-element state addressed by key, so scroll offsets, focus and animation phase survive a rebuild. Element state lives in that store, never in the widget value.full (00b331ad)Widget.key; state.d ElementStore/elementKeys
WGT6Text must support styled runs within a single node — a sequence of (text, slot) spans — so syntax-highlighted content is expressible directly, without a backend overpainting the toolkit's own output to re-colour it.partialWidgetKind.rich + TextSpan end to end (a6c6c69f), wrapping included (wrapSpans, 407bce58 — the twoslash docs/tag paths now wrap as rich runs with inline pills); the GUI signature overpaint retires when the view emits highlighted rich signatures

NOTE

WGT5 deliberately separates two different relationships. Equality decides "may I skip repainting"; identity decides "is this the same element, so its state carries over". Conflating them is the classic reconciliation bug, and keeping element state out of the widget value removes one source of dishonest equality. Totality and copy independence still depend on every payload meeting PRN6.

Component catalog (WGT7+)

Each row is a view model plus its view. Status reflects the toolkit, not any one consumer.

IDComponentStatusNotes
WGT7Containers — row, column, stack, panel, popupfullshipped
WGT8Primitives — box, text, glyph, linefullshipped
WGT9Scroll view — clipped viewport with an offsetfullcomponents/chrome.d scrollView over LAY7 + ScrollState + Widget.key
WGT10Scrollbar — track and thumb, hover/drag affordancepartialcomponents/chrome.d scrollbar over STM2's one formula; hover/drag affordance wires up with the M9 chrome port
WGT11Table — columns with alignment and spans, header, optional borderspartial (e78f404c) — the markdown view renders tables over the track sizer with aligned fixed-width cells and source-anchored cell keys; a standalone table widget is still openview model over LAY9's track sizer
WGT12Tree — the exemplar; flat arena, opened set, guides, lazy childrenpartialcomponents/tree_widget.d (4e3ad035); lazy children (VMD5) arrive with the explorer
WGT13List — selectable rows, optional virtualizationnot starteddegenerate tree; shares the selection machine
WGT14Text input — caret, editing, submissionnot startedtier 1
WGT15Button — label, press state, activationpartial (IXB9): PressState (STM10) + the actionBar segmented band; a standalone button view still to cometier 1
WGT16Toast / notification — transient, timed or event-scopednot startedview over STM6
WGT17Header / status bar — leading, centre and trailing segment groupsfullcomponents/chrome.d headerBar (grow-spacer distribution, chrome slot band)
WGT18Gutter — line numbers, markers, fold indicatorspartialcomponents/chrome.d gutter (numbers, LAY8-aligned); markers/fold indicators come with the document view
WGT19Meter / progress — determinate and indeterminatenot startedindeterminate is a mode, not a sentinel value
WGT20Divider / spacernot startedspacer is a grow box, per LAY8
WGT21Link — activatable reference; hyperlink escape on capable terminalsnot startedneeds a link concept in the visual vocabulary
WGT22Image / media — sized placeholder with per-target realisationnot starteddegrades to alt text
WGT23Tabs — tab bar plus one visible panelpartial — chrome.d tabStrip: label-sized or growing segments, active distinguished from armed, hits from the laid-out frames; the panel is the caller's (hue's document set, markdown code groups)tier 0 on HTML via checked-radio idiom
WGT24Disclosure — collapsible region with a placeholderpartial (9fc03551) — the markdown fold placeholder over STM5 (both interactive backends); a generic disclosure widget is still opentier 0 on HTML; shares STM5
WGT25Task list — ordered items with status marks and a running/blocked distinctionnot startedview model is presentation-free; its driver is not — see below

IMPORTANT

The live region is not a widget, and must not become one. It repaints the bottom of a scrolling terminal in place: it writes cursor-control escapes to a stream and owns output sequencing. That is a line-oriented incremental output sink, not a canvas — it has no rectangle, no clip and no frame. Putting it behind isCanvas would violate the canvas-first posture (UIA1) and force every backend to pretend it has a cursor.

The split: a task list's view model (items, statuses, ordering) is presentation-free and belongs here as WGT25; the live region and the reporter that drives it stay a terminal concern owned by the cell backend's package. Spinner and progress glyphs are theme data (THM); the meter/progress view is WGT19.

Tree component (VMD1VMD6)

The exemplar of the view-model/view split.

IDRequirementStatusTraces to
VMD1Tree data must be a flat node arena with parent/child/sibling indices, independently snapshot-able as a value, holding no interaction state and no decoration.partialflat arena and separation shipped (4e3ad035); explicit copy/alias semantics for its slice and T remain UI-O1
VMD2Tree interaction state — opened set, selection, scroll offset — must live in a separate value keyed by node identity (a path of identifiers), so one tree can back several independent views.partialDisclosureState + caller-owned selection (4e3ad035); path-keyed identity lands with the explorer's stable keys
VMD3Flatten must be a pure free function (data, state) → range of (depth, node, isLastChild), with no lazy loading and no filtering mixed in. It must be testable in isolation.full (4e3ad035)tree_widget.d flatten
VMD4Guide characters must follow the four-state model — space, continue, fork, end — accumulated per depth level, with the per-depth state precomputed during flatten rather than recomputed per render.full (4e3ad035)tree_widget.d Guide/FlatTreeRow.guides
VMD5Lazy children must separate user intent ("this should be open") from loaded state ("children have been read"), so the tree knows what should be expanded before it has read it.full (d47a0d01) — the explorer's open (DisclosureState intent) / expanded (children read one level past open) splitproposed lazy provider
VMD6Node capabilities — has children, has an icon, has a status badge — must be detected by introspection, so a filesystem tree and a syntax-tree share one renderer without a type hierarchy.partialtreeView introspects optional label/icon/slot (4e3ad035); status badges join with the explorer's git decoration

NOTE

An alternative traversal mode, where the visible tree is rebuilt as a function of (source, filter, depth limit) with no persistent expand state, is the natural fit for live filtering and coexists with VMD2 rather than replacing it. Flat storage is what makes rebuilding per keystroke viable.

Milestones

MilestoneScopeStatusRequirements
W0Sum-typed payload, Regular props, keys and element statepartial (00b331ad; payload sum is UI-O2, copy policy is UI-O1)WGT3WGT5
W1Styled-run text; hit identity through the pipelinefull (a6c6c69f, f166e099)WGT6
W2Chrome components — scroll view, scrollbar, header/status, gutterpartial (views shipped; hue consumes them in M9)WGT9WGT10, WGT17WGT18
W3Content components — table, list, rich textnot startedWGT11, WGT13
W4Tree component per the case studypartial (4e3ad035; VMD5 with the explorer)WGT12, VMD1VMD6
W5Interactive components — input, button, tabs, disclosure, toastnot startedWGT14WGT16, WGT23WGT24
W6Media and linksnot startedWGT21, WGT22

Module coverage

Source fileRequirements
libs/ui/src/sparkles/ui/widget.dWGT1WGT8
libs/ui/src/sparkles/ui/components/WGT9WGT24, VMD1VMD6
libs/ui/src/sparkles/ui/state.dWGT5 (element-state store)

Relationship to existing specs

PieceRole
Tree-view case studythe design record behind VMD1VMD6
layout.md LAYthe sizing, clipping and track facilities components need
state-machines.md STMthe behavior half of every interactive component
theme.md THM2the widened slot vocabulary the catalog requires
input.md INP5the tier a component declares
principles.md PRN1, PRN5, PRN6, PRN12ownership, sum-payload and Regular-value rules
open-issues.md UI-O1, UI-O2deferred ownership/copy and widget-sum implementation gaps

Overview · Layout · State machines · Theme