TUI Stage 4: the widget layer#34
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Stage 4 of the Flutter-style terminal UI framework in
app/lib/src/tui/— the reactive widget layer, a transcription of Flutter'sframework.dart:StatelessWidget/StatefulWidget/StatewithsetState,BuildContext.updateChild,Widget.canUpdate, the keyedupdateChildrenlist-diff, andLocalKey/ValueKey/ObjectKey/UniqueKey.InheritedWidget—O(1)ancestor lookup, dependency tracking,updateShouldNotify-driven rebuilds.RenderObjectWidgetbridge —Leaf/SingleChild/MultiChildvariants that create, update, and splice the Stage 3 render tree, plus concrete widgetsText/Padding/ConstrainedBox/SizedBox/DecoratedBox/Flex/Row/Column/Expanded/Flexible.BuildOwner— depth-ordered rebuild scheduler.runApp/TuiBinding— entry point driving frames over the existingTerminal+PipelineOwnerpipeline, with aTerminalAppInheritedWidgetexposing the key stream, terminal size, andexit().Render-tree amendment:
RenderFlexgainsinsert/movefor 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.mdImplementation plan:
docs/superpowers/plans/2026-05-15-tui-stage4-widget-layer.mdTest plan
flutter test— all 329 tests pass (55 new widget-layer tests covering keyed reconciliation, re-entrantsetState,InheritedWidgetdependency invalidation,ParentDataWidgetflex, theTuiBindingbuild→layout→paint pipeline)flutter analyze— cleandart tool/prepare_submit.dart— no diffcd app && dart run examples/tui/widget_demo.dartin a real terminal — keypress updates only the left panel, resize re-lays out,qexits cleanly🤖 Generated with Claude Code