Reflective property editors
A property tree is the component that takes a value of type T, produces one label/editor row per field, and lets a nested value be opened inline as a subtree. Ten systems were read to answer one question — what does a design commit to when it makes that tree? — because sparkles:ui must answer it once, in integer cells, for a GPU window, a terminal grid, a script-free HTML page and an Android NativeActivity, and with compile-time reflection instead of the runtime reflection most surveyed subjects rely on.
The corpus was picked so the answers would disagree, in two passes. Tier 1 took the four closest analogues that could be read from source plus the field's reference design: a library with no reflection at all (Qt's property browser), an editor panel whose tree is its widget hierarchy (Godot), a grid that delegates nesting to the model type's converter (WinForms), a details panel built around a property handle (Unreal, documentation only), and an immediate-mode inspector with no model whatsoever (bevy-inspector-egui).
Tier 2 was then chosen against the gaps that pass exposed rather than from the original shortlist: Unity to get the handle and multi-object axes from source where Unreal could not be read; the Rust derive-macro crates, the only compile-time-dispatch family and the closest peer to a D __traits descent; VS Code's settings editor, because every Tier-1 subject was a developer inspector and none was an end-user settings surface; react-jsonschema-form for the schema-driven family; and the DevTools object inspector for a live, foreign, cyclic graph. Four Tier-1 conclusions did not survive that second pass; they are listed as retractions rather than quietly amended.
This survey answers twelve questions
- Where does the tree live? — data model, widget hierarchy, serialized mirror, another process, or nowhere at all.
- What identifies a node, and how does it survive a rebuild? — seven answers, from a raw pointer to a remote lease. Vocabulary in
concepts.md. - Who decides that a value is a subtree? — converter, discriminant, manager, trait const, schema
type— or a refusal to descend at all. - How does anyone survive a cycle? — and the rule Tier 2 supplied: guard the walk that is neither user- nor data-driven.
- When does typing become a value? — commit points, transient vs committed, and where validation is shown.
- How is a sum-typed field edited? — pickers, constructability, and the one subject that migrates data across a switch.
- What does multi-object editing force into an architecture? — four positions, from designed-in to incomplete.
- Which features actually require a retained tree? — the frame-model question, as a table.
- What does a design assume about its surface? — pixels, hover, modals, a frame clock, a live runtime.
- What must a Sparkles design decide? — eight forks, each re-run against the Tier-2 evidence.
- What did Tier 2 retract? — four Tier-1 claims withdrawn, two narrowed, two upheld.
- How should a property tree behave at scale? — virtualization, pagination, fetch policy, and recursive bucketing.
Last reviewed: August 19, 2026
IMPORTANT
Nine subjects were read from source at a pinned revision recorded in the revision ledger; every claim about them cites a file and line at that revision. Unreal could not be read from source — the repository requires an authenticated account — and is documented from archived API reference pages, marked as such on every page it appears. Unity is source-readable only at its managed layer; native internals are marked INFERENCE. Statements that are analysis rather than observation are marked INFERENCE throughout.
Master catalog
Tier-2 subjects are marked ★.
| Subject | Ecosystem | Tree lives in | Metadata | Multi-object | Undo | Deep-dive |
|---|---|---|---|---|---|---|
| Qt Property Browser | C++ / Qt Widgets | independent data model | none — caller builds it | no | no | qt-property-browser.md |
Godot EditorInspector | C++ / Godot editor | the widget tree | runtime PropertyInfo stream | intersection, no mixed marker | yes (EditorUndoRedoManager) | godot-inspector.md |
WinForms PropertyGrid | C# / .NET | retained entry model | runtime TypeDescriptor | merged descriptors, blank when mixed | delegated to designer host | winforms-propertygrid.md |
| Unreal Details panel (docs only) | C++ / Slate | node tree behind handles | UPROPERTY + meta= map | first-class, per-object values | automatic via handle | unreal-details-panel.md |
bevy-inspector-egui | Rust / egui | nowhere — a stack frame | type registry of options | ui_for_reflect_many | no | bevy-inspector-egui.md |
★ Unity SerializedProperty | C# / Unity Editor | serialized mirror + cursor | serialization + attributes | hasMultipleDifferentValues, ambient showMixedValue | on ApplyModifiedProperties | unity-serializedproperty.md |
| ★ Derive-macro inspectors | Rust / egui | the generated call graph | attributes, at compile time | no | no | derive-macro-inspectors.md |
| ★ VS Code settings editor | TypeScript / DOM | settings tree model | JSON-schema contributions | scope selector instead | no (the text editor's) | vscode-settings-ui.md |
★ react-jsonschema-form | TypeScript / React | nowhere — per render | a schema + a uiSchema | no | host's | react-jsonschema-form.md |
| ★ DevTools object inspector | TypeScript / DevTools | the other process | CDP descriptors | no | no | devtools-object-inspector.md |
sparkles:ui (the baseline) | D | per-frame tree + explicit state values | UDAs, unused for this | no | no | sparkles-baseline.md |
Taxonomies
By architectural family
| Family | Subjects | Commits a design to |
|---|---|---|
| Model-first | Qt, Unreal, ★VS Code | a second structure and its lifetime — in exchange for several presentations, model-level search, and multi-object addressing you already have |
| View-first with external state | Godot, WinForms, ★Unity | naming every piece of state that must outlive a refresh — and losing whatever you failed to name |
| Function-first | bevy, ★derive crates, ★rjsf | positional identity (unless you mint keys) and a frame clock — in exchange for no rebuild, invalidation or notification |
| ★ Handle-first over a foreign graph | ★DevTools | asynchrony, staleness and never knowing the row count — in exchange for inspecting something unbounded and live |
Detail, and the placement of sparkles:ui, in comparison.md § families.
By descent decision
| Owner of the decision | Subject | Consequence |
|---|---|---|
| the model type's converter | WinForms | any type becomes inspectable everywhere without touching the component |
| the value's structural kind | bevy | uniform, with a leaf-editor registry consulted first |
| the value's runtime type + hint | Godot | fully dynamic, re-derived every rebuild |
| the property's manager | Qt | total control, and a hand-written manager per composite type |
| ★ a trait const / overridden visitor | ★derive crates | compile-time, no registry — and the orphan rule blocks foreign types |
★ the schema's type | ★rjsf | conditions and cycles are expressible in the data |
| ★ nobody — the GUI declines | ★VS Code | Complex values are handed to the text editor over the real format |
| the static type, at compile time | D / Sparkles | a manifest constant — and a hard build error on a recursive type |
By how the walk is bounded
| Strategy | Subject | What it accepts |
|---|---|---|
| eager, whole subtree | Qt | a large collapsed model still costs its full item tree |
| lazy on expand | WinForms, Godot, ★Unity | unbounded manual descent into a self-referential value |
| per frame, only what is open | bevy, ★derive crates, ★rjsf | everything visible is re-walked at frame rate |
| ★ visited set — but only in automatic walks | ★Unity, ★rjsf | the guard's cost, paid exactly where a human is not driving |
| ★ fetch policy (200 rows, 100-element buckets) | ★DevTools | the reader sees a truncated truth and must ask for more |
| ★ refuse to descend | ★VS Code | a visible capability cliff, with an escape to the serialization format |
| per frame, windowed to the viewport | sparkles:ui | (the current baseline: flatten + viewSlice) |
Revision ledger
Compilers used for the runnable examples: ldc2 1.41.0 (D 2.111) and dmd 2.112.1. No Rust toolchain is available in this repository's dev shell, so the Rust subjects were read but not compiled.
Runnable examples
Eleven single-file dub programs, compiled and run by the repository's ci helper. The first four back a claim the prose makes; the last seven are the design spikes written after the survey, against the real sparkles:ui components, and two of them retract a survey claim.
| Example | Backs |
|---|---|
examples/reflect-descent.d | compile-time descent yields a manifest row plan; a recursive type is a build error without a visited-type set |
examples/sumtype-variants.d | variant switching in D: .init makes constructability nearly free, but SumType.opAssign is @system when another member has indirections |
examples/uda-metadata.d | what a UDA channel answers at compile time, and why a value-dependent condition must be carried as data |
★ examples/erased-descent.d | the Tier-2 escape as first understood: an erasure boundary in the child walk, descent by budget, a cut with an Expand affordance — superseded on the cost claim by type-only-instantiation.d below |
Design spikes
Written while designing the component, so each one names the fork it settles:
| Spike | Settles |
|---|---|
examples/open-set-descent.d | descent driven by the opened set over the real TreeData/DisclosureState: a cyclic subject terminates, expansion survives rebuild, and DisclosureState.allOpen() needs a depth cap |
examples/type-only-instantiation.d | retracts the build-failure claim: the CTFE limit was path-parameterised instantiation, not recursion — a type-only walk yields a compile-time manifest for a cyclic type, no delegate |
examples/leaf-dispatch.d | D2/D4: a closed static if ladder to LeafKind, a UDA vocabulary, @ShowIf("expr") as a typed @safe predicate, and the opaque-value escape |
examples/path-addressing.d | D3: a.b[2].c resolved at compile time (a ref-returning access) and at run time (a generated walk), proved equal by differential test — and index paths shown to be positional |
examples/edit-commands.d | D5: an edit is a value, applying it returns its inverse, @readOnly and the read-only policy refuse inside the dispatch — and the SumType @system rule is shown to be directional |
examples/erased-subject.d | D1's dynamic half: one walk over a subject that is half static, half type-erased, with propChildren/propExpandable/propText detected by presence |
examples/tree-adapter.d | the component as an adapter over TreeData + TreeViewState + activate/collapseOrUp/treeText — and the one toolkit gap: hasChildren is structural |
Suggested reading paths
- "I want the map, not the details."
comparison.md§ matrices → § families → § retractions → § surprises. - "I am about to write the spec."
sparkles-baseline.md(the delta table) →comparison.md§ decisions → the four examples. - "I care about compile-time reflection specifically." ★derive crates →
reflect-descent.d→erased-descent.d→uda-metadata.d. - "I care about cycles and big graphs."
comparison.md§ 4 → ★DevTools → ★rjsf → ★Unity. - "I care about multi-object editing." Unreal → ★Unity → WinForms § multi-object.
- "I care about settings, not inspection." ★VS Code → ★rjsf.
What this survey does not cover
Deliberate scope limits, stated so they are not mistaken for findings:
- Demoted to matrix-row status, unread. Xceed WPF Toolkit
PropertyGrid(a WPF restatement of the WinForms design), ControlsFXPropertySheet(a thin bean-over-Itemlayer), NetBeansNode.Property(structurally Qt's manager), andreact-json-tree(subsumed by ★DevTools). No claim in this tree rests on them. - Dropped from the original Tier-2 shortlist.
NSOutlineView,GtkTreeViewand JavaFXTreeTableView— tree-table mechanics are already covered in-repo by the tree-view case study and the shipped tree component. Generic Dear ImGui/eguiidioms — superseded by the ★derive crates. - Parked. Delphi/Lazarus
TOIPropertyGrid(the oldest RTTI-driven inspector) and Blender's RNA-driven buttons — both interesting, neither answering a question the ten subjects left open. - Unreal was not read from source; laziness, cycle handling and optional/nullable treatment could not be determined and are recorded as unknown rather than guessed.
- Unity's native layer was not read, only its published C# reference.
- No behaviour was exercised at runtime in any surveyed subject; every claim is a source or documentation reading. The only executed code in this tree is the D examples.
Sources
Per-subject sources are listed in each deep-dive's Sources section; revisions are in the ledger above. In-repo context: docs/specs/ui/inspector.md (the inspector component this would feed), docs/specs/ui/editor.md (the editable-text component, not started), and docs/research/anchored-overlays/ for the surface-independence method this tree reuses.