Skip to content

Sparkles baseline (sparkles:ui)

What the toolkit already answers on each spine dimension, read from the working tree, and the delta between that and what a reflective property editor needs.

Subjectlibs/ui (sparkles:ui), libs/ui-app, with libs/input
Read atworking tree of branch feat/ui/property-tree, parent commit 77d0d547
Frame modelview → layout → buildDisplayList → paint, rebuilt per frame
Retained stateexplicit value types the host owns (TreeViewState, DisclosureState, LineEditState, ScrollbarState, …)
Reflectioncompile-time only (__traits, std.traits, UDAs)
Targetsraylib GPU, terminal cell grid, script-free HTML, Android

The frame model, stated precisely

presentApp rebuilds the widget tree every frame and lays it out from scratch:

d
snap = FrameSnapshot.init;
snap.tree = app.view(h);

snap.frames = layout(snap.tree, Constraints(sz.width, sz.height));
buildDisplayListInto(snap.tree, snap.frames,, h.ops());

libs/ui-app/src/sparkles/ui_app/run_app.d:160

So on the comparison's frame-model axis, Sparkles is already on the immediate-mode side of the corpus — closer to bevy-inspector-egui than to any of the retained subjects. It differs in one decisive respect: the toolkit does not have an id-keyed side table like egui's. Everything that must persist is a named value the host stores, which is why sparkles:ui has an explicit vocabulary of interaction-state structs instead of an implicit memory.

That is the single most important baseline fact for the design: a property tree here cannot "just work" the way an egui inspector does, because there is no ambient place to put a row's expansion or a field's in-progress text. Either the component names that state as a value, or it does not have it.

Model & addressing

The tree component is already split three ways, and the split is documented as the design insight it came from:

  • dataTreeData, the flat arena. Owned by the adapter, rebuilt at will.
  • interaction (TreeViewState) — the opened set, the cursor, the viewport and both scrollbars, and the live filter's editor: every piece of state a tree pane keeps between frames, as one value.
  • viewtreeView over the viewSlice window this state selects.

libs/ui/src/sparkles/ui/components/tree_view.d:1

  • TreeData(T) (components/tree_widget.d:40) is a flat arena of (value, parent, firstChild, nextSibling) — indices, not pointers.
  • TreeViewState(Key) (components/tree_view.d:79) is generic over the adapter's node identity, "so the opened set survives rebuilds" — the corpus's rebuild-survival problem, already solved once, by making identity the adapter's choice rather than the component's.
  • DisclosureState(Key) (state.d:1171) stores expansion as defaultOpen plus a sorted exception set, so "expand all" is a polarity flip rather than an O(n) walk.
  • TreeStep (components/tree_view.d:62) is how interaction reports structural invalidation: rebuild means "the opened set or filter changed — rebuild rows", and the adapter does it. The component never rebuilds anything itself.

Against the corpus, this is Unreal's "identity survives the rebuild" property obtained without a handle object: the key is a value the adapter mints.

Metadata

Nothing exists for property metadata. The toolkit's DbI convention is capability-by-presence on the adapter's node type — the inspector view reads a node's label/badge only if they compile:

d
static if (__traits(compiles, { const(char)[] s = v.label; }))

libs/ui/src/sparkles/ui/components/inspector.d:323

The in-repo precedents for a UDA metadata vocabulary are sparkles:core-cli's @CliOption and sparkles:wired's compile-time-reflected wire format. uda-metadata.d measures what that channel can answer: label/group/range/hidden are compile-time constants, but any condition over the value has to be carried as data (a function pointer in the UDA) and evaluated per frame — which is the compile-time analogue of Godot's _validate_property rebuild.

Recursion

flatten already materialises only what is open, and only in pre-order:

d
if (data.hasChildren(at) && isOpen(at))
    walk(data.nodes[at].firstChild, depth + 1);

libs/ui/src/sparkles/ui/components/tree_widget.d:115

So the visible-row discipline the corpus reaches for is in place. What is absent is everything upstream of it: nothing decides that a type has children, nothing builds TreeData from a T, and nothing knows what a value's editor is.

The D-specific finding is that the descent decision moves to compile time and takes the cycle problem with it. reflect-descent.d demonstrates a static if (isAggregateType!Target) walk that yields a manifest constant — and shows that the visited-type set is not optional: without it, struct Node { Node* parent; } does not compile, with

Error: template instance ... recursive expansion exceeded allowed nesting limit

on both ldc2 1.41 (D 2.111) and dmd 2.112. Where Godot and WinForms let a reader unfold a self-referential value forever, and bevy-inspector-egui gets cycle-freedom from Rust's aliasing rules, a compile-time descent in D gets a hard build error — a stronger guarantee, and a constraint the design cannot ignore. Note also what the example's output shows: a type-visited cut still expands the recursive type once (a full duplicate subtree under parent.) before cutting, so "cut on second occurrence" is a policy choice with a visible row-count cost.

Editing & mutation

Nothing to build on yet, and the gap is bigger than it looks:

  • There is no text editor. LineEditState (state.d:990) is a single-line, append/backspace machine used for the incremental-search field. The real editable-text component is specified but not starteddocs/specs/ui/editor.md is entirely not started, and its scope note says hue's "only text input today is the single-line incremental-search field, hand-rolled per backend".
  • There is no command, transaction or undo vocabulary anywhere in sparkles:ui.
  • There is no change-notification mechanism, and per the frame model none is needed: the next frame re-reads the subject.
  • Mutation would be direct, in-place, through whatever reference the view holds — the bevy-inspector-egui posture, with @safe consequences the corpus never had to think about. sumtype-variants.d measures one: Phobos' SumType.opAssign is @system whenever another member type has indirections, so a variant picker over an arbitrary SumType needs a @trusted seam whose precondition is that nothing holds a pointer into the old payload.

Type coverage

KindBaseline
Structs / classesTreeData can express the shape; nothing produces it from a type
Collectionssparkles:ui's table core exists (components/table/) with freeze panes and overflow policy, but no add/remove/reorder editing model
Sum typesnone; D offers SumType, class hierarchies and hand-rolled tagged unions, all with different variant-switch semantics
Optionalnone; Nullable, Expected!(T, E) and pointers are three different "unset"s
Opaquenone

Presentation & control

  • Grouping/ordering — declaration order is what __traits(allMembers) gives; there is no category vocabulary.
  • FilterTreeViewState already owns a live filter whose every edit asks the adapter to rebuild, which is the same "filter changes the tree" model Godot uses (and Godot additionally disables folding while filtering, editor_inspector.cpp:4464).
  • VirtualizationviewSlice windows the visible rows and the display list carries only those; the table core does the same for columns. This is WinForms-grade row virtualization, already present.
  • Multi-object editing — nothing.
  • Escape hatches — the toolkit's convention is DbI capability-by-presence plus adapters, not a registry. There is no dynamic dispatch surface to register an editor into, and adding one would be the first of its kind in sparkles:ui.

Delta table

Each row: the capability, where the corpus's answer comes from, and what Sparkles has today.

CapabilityCorpus answerSparkles todayDelta
Presentation-free node modelQt QtProperty; Unreal FPropertyNodeTreeData(T) flat arenahave
Node identity that survives a rebuildUnreal handles; WinForms entry diffing; Godot fold-state on the objectTreeViewState(Key) keyed by adapter identityhave (identity is the adapter's to mint)
Expansion as a valueGodot stores it on the edited objectDisclosureState(Key) with polarity + exceptionshave, and better
Visible-row materialisationall four retained subjectsflatten descends only into open nodes; viewSlice windowshave
Row virtualizationWinForms _visibleRowsviewSlice + display listhave
Type → rows (reflection)runtime in seven subjects; compile-time in the derive cratesnothingmissing — and compile-time here, with a hard recursion limit
Descent decisionconverter (WinForms) / discriminant (bevy) / hint (Godot)nothingmissing
Metadata channelattributes / PropertyInfo / registryUDAs exist as a mechanism (@CliOption precedent)missing as a vocabulary
Leaf editor dispatchfactory (Qt) / registry (bevy) / converter+editor (WinForms)nothing; no dynamic dispatch surface in the toolkitmissing
Text editingevery subjectLineEditState only; the editor component is not startedmissing — blocking for any string field
Transient vs committed editsGodot changing; Unreal SetValue flagsnothingmissing
Undo / transactionsGodot EditorUndoRedoManager; WinForms designer transactionnothingmissing
Validation + error displayWinForms modal; VS Code inline per row; rjsf path-addressed errorsnothingmissing
Collections editingUnreal AsArray; bevy list ops; Godot paged; rjsf stable row keystable core, read-onlymissing
Variant / type pickerbevy constructable check; derive crates generate the construction; rjsf migrates datanothing; SumType assignment is @systemmissing, with a D-specific safety wrinkle
Multi-object editingWinForms merged descriptors; Unity ambient showMixedValue; Unreal per-object valuesnothingmissing
Conditional visibilityattributes (WinForms) / usage flags (Godot) / restrictions (Unreal) / schema if-then (rjsf)nothingmissing
Element identity in a collectionrjsf mints a synthetic row key; everyone else uses the indexnothingmissing — and the corpus says the index is not enough
Provenance (default / scope / locked)VS Code isConfigured, defaultValueSource, hasPolicyValuenothingmissing — needed the moment this edits config rather than a value
Bounding a hostile subjectDevTools fetch policy; Godot paginationviewSlice windows rows already materialisedpartial — no policy before materialisation
Refusing to descend, with an escapeVS Code Complex → edit the JSONnothingmissing — the obvious answer for the read-only HTML target
An erasure boundary in the child walkderive crates &mut dynnothingmissing — the escape from our compile-time recursion limit (erased-descent.d)

Three rows deserve emphasis because they cross a target or language boundary rather than merely being unbuilt:

  • Text editing is the gating dependency. Three of the four targets can host an editable field; the script-free HTML target is read-only by doctrine (docs/specs/ui/editor.md), so a property tree that assumes an editor everywhere cannot serve it. A read-only presentation is not a degraded mode there — it is the only mode.
  • The recursion limit is ours alone. Every surveyed subject descends at runtime, so a self-referential type costs them a click; a D CTFE walk does not build at all (reflect-descent.d). The corpus's escape — an erasure boundary in the child walk (derive crates) — is measured for D in erased-descent.d: it works, at one delegate per open node.
  • Editor dispatch has no existing shape in this toolkit. Every corpus answer is a runtime registry; sparkles:ui's entire idiom is compile-time capability detection. Whatever the design picks will be either the toolkit's first registry or the corpus's first compile-time dispatch — see comparison.md § decisions.

Sources

In-tree, at the working tree of feat/ui/property-tree (parent 77d0d547):

  • libs/ui/src/sparkles/ui/components/tree_view.d — the three-layer split, TreeViewState, TreeStep
  • libs/ui/src/sparkles/ui/components/tree_widget.dTreeData, flatten, treeView
  • libs/ui/src/sparkles/ui/components/tree_model.d — the pure flatten-to-rows function
  • libs/ui/src/sparkles/ui/components/inspector.d — the adapter contract and capability-by-presence
  • libs/ui/src/sparkles/ui/state.dDisclosureState, LineEditState, ScrollbarState, CaptureState
  • libs/ui-app/src/sparkles/ui_app/run_app.d — the per-frame view → layout → display list pipeline
  • docs/specs/ui/editor.md, docs/specs/ui/inspector.md — the editable-text component (not started) and the inspector requirements
  • Runnable: examples/reflect-descent.d, examples/sumtype-variants.d, examples/uda-metadata.d