Skip to content

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

  1. Where does the tree live? — data model, widget hierarchy, serialized mirror, another process, or nowhere at all.
  2. 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.
  3. Who decides that a value is a subtree? — converter, discriminant, manager, trait const, schema type — or a refusal to descend at all.
  4. How does anyone survive a cycle? — and the rule Tier 2 supplied: guard the walk that is neither user- nor data-driven.
  5. When does typing become a value? — commit points, transient vs committed, and where validation is shown.
  6. How is a sum-typed field edited? — pickers, constructability, and the one subject that migrates data across a switch.
  7. What does multi-object editing force into an architecture? — four positions, from designed-in to incomplete.
  8. Which features actually require a retained tree? — the frame-model question, as a table.
  9. What does a design assume about its surface? — pixels, hover, modals, a frame clock, a live runtime.
  10. What must a Sparkles design decide? — eight forks, each re-run against the Tier-2 evidence.
  11. What did Tier 2 retract? — four Tier-1 claims withdrawn, two narrowed, two upheld.
  12. 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 ★.

SubjectEcosystemTree lives inMetadataMulti-objectUndoDeep-dive
Qt Property BrowserC++ / Qt Widgetsindependent data modelnone — caller builds itnonoqt-property-browser.md
Godot EditorInspectorC++ / Godot editorthe widget treeruntime PropertyInfo streamintersection, no mixed markeryes (EditorUndoRedoManager)godot-inspector.md
WinForms PropertyGridC# / .NETretained entry modelruntime TypeDescriptormerged descriptors, blank when mixeddelegated to designer hostwinforms-propertygrid.md
Unreal Details panel (docs only)C++ / Slatenode tree behind handlesUPROPERTY + meta= mapfirst-class, per-object valuesautomatic via handleunreal-details-panel.md
bevy-inspector-eguiRust / eguinowhere — a stack frametype registry of optionsui_for_reflect_manynobevy-inspector-egui.md
Unity SerializedPropertyC# / Unity Editorserialized mirror + cursorserialization + attributeshasMultipleDifferentValues, ambient showMixedValueon ApplyModifiedPropertiesunity-serializedproperty.md
Derive-macro inspectorsRust / eguithe generated call graphattributes, at compile timenonoderive-macro-inspectors.md
VS Code settings editorTypeScript / DOMsettings tree modelJSON-schema contributionsscope selector insteadno (the text editor's)vscode-settings-ui.md
react-jsonschema-formTypeScript / Reactnowhere — per rendera schema + a uiSchemanohost'sreact-jsonschema-form.md
DevTools object inspectorTypeScript / DevToolsthe other processCDP descriptorsnonodevtools-object-inspector.md
sparkles:ui (the baseline)Dper-frame tree + explicit state valuesUDAs, unused for thisnonosparkles-baseline.md

Taxonomies

By architectural family

FamilySubjectsCommits a design to
Model-firstQt, Unreal, ★VS Codea second structure and its lifetime — in exchange for several presentations, model-level search, and multi-object addressing you already have
View-first with external stateGodot, WinForms, ★Unitynaming every piece of state that must outlive a refresh — and losing whatever you failed to name
Function-firstbevy, ★derive crates, ★rjsfpositional identity (unless you mint keys) and a frame clock — in exchange for no rebuild, invalidation or notification
Handle-first over a foreign graphDevToolsasynchrony, 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 decisionSubjectConsequence
the model type's converterWinFormsany type becomes inspectable everywhere without touching the component
the value's structural kindbevyuniform, with a leaf-editor registry consulted first
the value's runtime type + hintGodotfully dynamic, re-derived every rebuild
the property's managerQttotal control, and a hand-written manager per composite type
★ a trait const / overridden visitorderive cratescompile-time, no registry — and the orphan rule blocks foreign types
★ the schema's typerjsfconditions and cycles are expressible in the data
★ nobody — the GUI declinesVS CodeComplex values are handed to the text editor over the real format
the static type, at compile timeD / Sparklesa manifest constant — and a hard build error on a recursive type

By how the walk is bounded

StrategySubjectWhat it accepts
eager, whole subtreeQta large collapsed model still costs its full item tree
lazy on expandWinForms, Godot, ★Unityunbounded manual descent into a self-referential value
per frame, only what is openbevy, ★derive crates, ★rjsfeverything visible is re-walked at frame rate
★ visited set — but only in automatic walksUnity, ★rjsfthe guard's cost, paid exactly where a human is not driving
★ fetch policy (200 rows, 100-element buckets)DevToolsthe reader sees a truncated truth and must ask for more
★ refuse to descendVS Codea visible capability cliff, with an escape to the serialization format
per frame, windowed to the viewportsparkles:ui(the current baseline: flatten + viewSlice)

Revision ledger

SubjectSourceRevisionDate read
Qt Property Browserqtproject/qt-solutions777e95ba69952f11eaec0adfb0cb987fabcdecb32026-08-18
Godotgodotengine/godot944a3c6cbbbb88284feebcb0603464cb175fa18e2026-08-18
WinFormsdotnet/winformsaf0c793d58f30c92a3e42b5fabb8fee1ffe147962026-08-18
bevy-inspector-eguijakobhellermann/bevy-inspector-eguiac6729854a97a9abcd7657b29d7356bdea63c568 (v0.37.0)2026-08-18
Unreal EngineUE 5.1 API reference (docs only, source access gated)archived 2023-02-04 / 2023-04-012026-08-18
★ UnityUnity-Technologies/UnityCsReference (C# reference only)225b0fbdb57cc17d094e8056b71f8314aba56f73 (6000.7.0a4)2026-08-19
egui-probezakarumych/egui-probe5ce68de11b9dcee3ff4f3d2d5f8492a39e1a79b4 (0.12.0)2026-08-19
egui_structpingpongun/egui_structb9549e51491e02a6c471e1cf7a6bd4b77bd87203 (0.4.2)2026-08-19
egui_inspectMeisterlama/egui_inspectf37e05e0e1c71108d4faa7de884fdaa5ad91debb (0.1.3)2026-08-19
enum2eguimatthewjberger/enum2egui02fa82557acffeb1c08c661f84fea48b4b3acc26 (0.34.1)2026-08-19
★ VS Codemicrosoft/vscode474a349ad5b745e512ef86b864d1c74f7264dd7a2026-08-19
react-jsonschema-formrjsf-team/react-jsonschema-formc8723f5f39d030776909667e74d217572757483c2026-08-19
★ DevTools front endChromeDevTools/devtools-frontend788f6469296bf2420bd95bb6d73d28a21439f3452026-08-19
sparkles:uithis repositoryfeat/ui/property-tree (parent 77d0d547)2026-08-18

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.

ExampleBacks
examples/reflect-descent.dcompile-time descent yields a manifest row plan; a recursive type is a build error without a visited-type set
examples/sumtype-variants.dvariant switching in D: .init makes constructability nearly free, but SumType.opAssign is @system when another member has indirections
examples/uda-metadata.dwhat a UDA channel answers at compile time, and why a value-dependent condition must be carried as data
examples/erased-descent.dthe 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:

SpikeSettles
examples/open-set-descent.ddescent 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.dretracts 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.dD2/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.dD3: 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.dD5: 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.dD1'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.dthe component as an adapter over TreeData + TreeViewState + activate/collapseOrUp/treeText — and the one toolkit gap: hasChildren is structural

Suggested reading paths

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), ControlsFX PropertySheet (a thin bean-over-Item layer), NetBeans Node.Property (structurally Qt's manager), and react-json-tree (subsumed by ★DevTools). No claim in this tree rests on them.
  • Dropped from the original Tier-2 shortlist. NSOutlineView, GtkTreeView and JavaFX TreeTableView — tree-table mechanics are already covered in-repo by the tree-view case study and the shipped tree component. Generic Dear ImGui/egui idioms — 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.