Skip to content

TUI Stage 4: the widget layer#34

Merged
xvrh merged 31 commits into
masterfrom
claude/quirky-mcnulty-c25a99
May 16, 2026
Merged

TUI Stage 4: the widget layer#34
xvrh merged 31 commits into
masterfrom
claude/quirky-mcnulty-c25a99

Conversation

@xvrh

@xvrh xvrh commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Stage 4 of the Flutter-style terminal UI framework in app/lib/src/tui/ — the reactive widget layer, a transcription of Flutter's framework.dart:

  • Widget / Element / State — immutable config tree, mounted-instance tree, StatelessWidget/StatefulWidget/State with setState, BuildContext.
  • Element lifecycle & reconciliation — mount/update/rebuild/unmount, updateChild, Widget.canUpdate, the keyed updateChildren list-diff, and LocalKey/ValueKey/ObjectKey/UniqueKey.
  • InheritedWidgetO(1) ancestor lookup, dependency tracking, updateShouldNotify-driven rebuilds.
  • RenderObjectWidget bridgeLeaf/SingleChild/MultiChild variants that create, update, and splice the Stage 3 render tree, plus concrete widgets Text/Padding/ConstrainedBox/SizedBox/DecoratedBox/Flex/Row/Column/Expanded/Flexible.
  • BuildOwner — depth-ordered rebuild scheduler.
  • runApp / TuiBinding — entry point driving frames over the existing Terminal + PipelineOwner pipeline, with a TerminalApp InheritedWidget exposing the key stream, terminal size, and exit().

Render-tree amendment: RenderFlex gains insert/move for child splicing, and its axis/alignment fields now trigger relayout on change.

Per the design, deferred to later stages: GlobalKey, a layer/repaint-boundary model, a focus/key-routing system, and animation/tickers.

Design spec: docs/superpowers/specs/2026-05-15-tui-stage4-widget-layer-design.md
Implementation plan: docs/superpowers/plans/2026-05-15-tui-stage4-widget-layer.md

Test plan

  • flutter test — all 329 tests pass (55 new widget-layer tests covering keyed reconciliation, re-entrant setState, InheritedWidget dependency invalidation, ParentDataWidget flex, the TuiBinding build→layout→paint pipeline)
  • flutter analyze — clean
  • dart tool/prepare_submit.dart — no diff
  • Manual smoke: cd app && dart run examples/tui/widget_demo.dart in a real terminal — keypress updates only the left panel, resize re-lays out, q exits cleanly

🤖 Generated with Claude Code

xvrh and others added 30 commits May 15, 2026 21:44
Specifies the reactive Widget/Element machinery transcribed from Flutter's
framework layer — Stateless/Stateful widgets, setState, the element lifecycle
and keyed reconciliation, InheritedWidget, RenderObjectWidget as the bridge to
the Stage 3 render tree, a BuildOwner scheduler, and a runApp entry point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eleven TDD tasks covering the RenderFlex splicing amendment, the Key/Widget/
Element/State machinery, BuildOwner, InheritedWidget, the RenderObjectWidget
bridge and keyed reconciler, concrete widgets, the runApp binding with its
headless test suite, exports/docs, and the StatefulWidget demo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements stage 4 plan tasks 3-5 as one compilable unit (the three
files cross-reference and the library cannot analyze until all exist):

- widget.dart: Widget, StatelessWidget, StatefulWidget, State.
  Omits ParentDataWidget (deferred to task 6).
- element.dart: BuildContext, Element + lifecycle (mount/update/
  rebuild/unmount/activate/deactivate), updateChild reconciliation,
  ComponentElement/StatelessElement/StatefulElement, _InactiveElements.
  Transcribed from Flutter's framework.dart, dropping GlobalKey, layers,
  focus, tickers, and debug machinery.
- build_owner.dart: BuildOwner depth-ordered rebuild scheduler.

dependOnInheritedWidgetOfExactType/getInheritedWidgetOfExactType are
stubbed (bound on Widget, lookup only) pending task 6's InheritedWidget.

Tests: element_lifecycle_test.dart (StatefulElement State wiring,
canUpdate sanity) and a canUpdate group appended to key_test.dart.
build_owner.dart has no test file here; its behavior is exercised by
the binding suite in plan task 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move super.performRebuild() (which clears _dirty) to run before build(),
so that any markNeedsBuild() issued during a widget's build() call is not
silently dropped by the `if (_dirty) return;` guard. StatefulElement is
unaffected: it clears _didChangeDependenciesFlag before delegating to
super, so didChangeDependencies() still precedes build().

A regression test is deferred to Task 9 with a detailed TODO: mounting
an element standalone requires root/binding infrastructure that task
provides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… didChangeDependencies

deactivate() no longer calls _unregisterDependencies(), leaving _dependencies
intact so activate() can detect that the element had inherited deps and must
re-run didChangeDependencies() after reactivation. _unregisterDependencies()
is now called only from unmount(), which also nulls _dependencies. Added
comments explaining the intentional asymmetry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements basic.dart with Text, Padding, ConstrainedBox, SizedBox,
DecoratedBox, Flex, Row, Column, Flexible, and Expanded — each wrapping
the corresponding Stage 3 render object. Also adds cell.dart and
painter.dart imports to widgets.dart so Color/HorizontalAlign/VerticalAlign
are visible to the widget layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements RootWidget/RootElement/TuiBinding/TerminalApp/runApp so the
widget tree can drive frames over the stage 3 render pipeline, and adds
the headless test suites that tasks 3-8 deferred (reconciliation,
build owner, inherited widgets, render-object bridge, parent data,
basic widgets, binding, deep element lifecycle).

Fixes a real bug surfaced by the re-entrant-rebuild regression test:
Element.rebuild asserted !_dirty after performRebuild, which crashed
when a setState issued during build() legitimately re-dirtied the
element. BuildOwner.buildScope already re-sorts and revisits such
elements, so the assert is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the broken StatefulWidget counter snippet in the TUI README with a
correct, self-consistent example that subscribes to keys in
didChangeDependencies (guarded by a bool), increments via setState, exits on
'q', and cancels the subscription in dispose — mirroring the real pattern in
widget_demo.dart. Also add a brief docstring to Element.activate noting it is
currently dormant in Stage 4, existing as forward-compat scaffolding for a
future GlobalKey / inactive-element-reuse stage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A flashy animated demo: a full-screen showcase reel cycling through five
scenes (plasma, starfield, charts, layout lab, typography), driven by a
Timer-based animation loop and a demo-defined custom Painted RenderObject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven tasks building app/examples/tui/widget_showcase.dart incrementally:
the Painted custom render object and animation loop, the five animated
scenes, the navigation chrome, keyboard input, and docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The shared harness was named _harness.dart; the repo's .gitignore has a
`_*` rule, so the file was silently never committed and the widget test
suites would fail on a fresh checkout. Rename to harness.dart and update
the eight importing test files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ParentDataElement applied its parent data from notifyClients, which
ProxyElement.update runs before the element rebuilds its child. When the
child changed widget type it was deactivated and re-inflated, so the new
render object never received the flex factor — it kept the default flex 0,
which a RenderFlex lays out with an unbounded main axis. In the widget
showcase that handed RenderPainted a ~1<<30 size and froze the demo on a
scene switch.

Apply parent data from performRebuild instead, after the base
ComponentElement.performRebuild has reconciled the child. Adds a regression
test for a type-changing Expanded child, and a defensive clamp in the demo's
RenderPainted so an unbounded axis can never again produce a runaway size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
runApp awaited the exit Completer and returned without ever tearing down
the element tree, so State.dispose() never ran. A State that started a
Timer (the widget showcase's animation loop) kept the Dart event loop
alive, so the process never terminated after 'q' — and the still-firing
Timer drew frames after the terminal was restored, scribbling artifacts
onto the recovered console.

Add TuiBinding.dispose() (and BuildOwner.unmountAll) to deactivate and
unmount the whole tree, running every State.dispose(). runApp now sets a
stopped flag to halt frames, then disposes the binding in its finally
block before the terminal is restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xvrh
xvrh merged commit 52f6b52 into master May 16, 2026
1 check passed
@xvrh
xvrh deleted the claude/quirky-mcnulty-c25a99 branch May 16, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant