feat(hytte-ui): declarative widget-tree → GTK reconciler (#195/#35 foundation)#199
feat(hytte-ui): declarative widget-tree → GTK reconciler (#195/#35 foundation)#199vibechoom wants to merge 1 commit into
Conversation
…for #195/#35) Add the decision-independent host-side reconciler for the out-of-process widget-plugin work ("frontend B"): a closed 7-node `Node` vocabulary plus a `Reconciler` that diffs a new tree against the previous render and mutates real `gtk::Widget`s in place via keyed diffing. - Keyed diffing: children with an `id` are matched across renders (reorder / prop change reuses the existing widget); id-less children fall back to positional matching. The diff decision is factored into a pure, GTK-free `plan_diff` covered by hermetic unit tests (insert / remove / reorder / keyed-vs-positional / kind-change / mixed). - Event hook: a single `on_event(NodeId, EventKind)` callback. Button click handlers (and id-bearing Box scroll controllers) are bound exactly once at widget creation, so widget reuse can never stack duplicate handlers. - GTK integration tests (constructing real widgets) are gated behind the `system-tests` feature and use `#[gtk::test]` so they share one GTK main thread. Sparkline's older manual-init `#[test]`s are aligned onto the same idiom (a plain `#[test]` lands on a libtest worker thread and can't coexist with `#[gtk::test]` in the same binary). Out of scope (gated on the design stamps for #195): transport/socket, encoding/serde, plugin supervisor, capability/effect broker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Adversarial-reviewed the diff — the foundation is sound. ✅ (draft; CI running) This is the decision-independent host-side reconciler from #195/#35 — What I verified (the parts that bite):
Robust to your 4 open sub-decisions: it keys on an explicit One thing worth a design call when you stamp #195 🧵: scroll is currently surfaced on any id-bearing Still gated on you before the next chunks (schema/SDK · transport · supervisor · effect-broker): the 4 sub-decisions + your two latest calls I folded in (full subscribed-state snapshot not deltas; supervision as a systemd unit). Left as a draft — your call to merge, @annikahannig. 🧱 |
What this is
The decision-independent host-side reconciler — the shared foundation for the out-of-process widget-plugin work ("frontend B", #195 / #35). A plugin describes its UI as an in-memory tree of
Nodes; the host (Reconciler) diffs each new tree against the previous render and mutates realgtk::Widgets in place via keyed diffing, instead of the naive "remove every child, rebuild" pattern (widgets/workspaces.rs,widgets/tray.rs).Lives in the new
crates/hytte-ui/src/widget_tree.rs, exported fromhytte-ui(Node,EventKind,NodeId,Dir,Reconciler).What's IN
Nodevocabulary — a closed 7-kind set:Box/Label/Icon/Button/Progress/Revealer/Separator.classesapply verbatim as CSS classes (the existingts-*/hytte-*token contract).Reconciler— children with anidare matched across renders (a reorder or prop change reuses the existing widget); id-less children fall back to positional matching. On match it updates in place (text / icon / fraction / reveal / class delta); on a new key it creates; on a vanished key it removes; order follows the new tree.on_event(NodeId, EventKind)callback (Click,Scroll { dx, dy }). Button click handlers (and scroll controllers on id-bearing Boxes) are bound exactly once at widget creation, so widget reuse can never stack duplicate handlers (no double-fire).plan_diff→ 13 hermetic unit tests run under the defaultcargo test(insert / remove / reorder / keyed-vs-positional / kind-change / mixed / partition invariant).system-testsfeature and run underxvfb-run.What's deliberately OUT
Gated on the design stamps for #195 — not in this PR:
This is purely an in-memory
Nodetree → GTK widgets, driven by explicitrender(&tree)calls. The transport layer will later driverenderand wireon_eventto outboundEventframes.Note on the test harness
GTK is single-threaded, so the GTK tests use
#[gtk::test](one shared GTK main thread). Sparkline's older manual-gtk::init()#[test]s are aligned onto the same idiom in this PR — a plain#[test]lands on a libtest worker thread (tripping "GTK may only be used from the main thread") and can't coexist with#[gtk::test]in the same binary.Gates (all green, in the Nix devShell)
cargo build✓cargo clippy --workspace --all-targets✓ (also-p hytte-ui --features system-tests✓) — zero warnings, pedantic-clean, nounsafecargo test -p hytte-ui✓ (20 hermetic)xvfb-run cargo test -p hytte-ui --features system-tests✓ (31 passed)nix fmtcleanRefs #195, #35.
🤖 Generated with Claude Code