Skip to content

fix: redraw the whole tree on every render pass [minor] - #113

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/nice-davinci-fufgxs
Sep 15, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/nice-davinci-fufgxs

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #109

The problem

UIApplication.Render() cleared the entire console on every pass, but UIElementBase.Render() only drew while IsDirty was still set — and IsDirty was cleared right after an element's first draw. The two mechanisms cancelled each other out: the screen was wiped every frame while only the elements the last input had invalidated were redrawn, so any static element vanished on the very next pass. A label beside a focused input disappeared on the first keystroke, which is the most ordinary layout the library has.

The fix

The issue asked for a deliberate choice between the two rendering models rather than a patch. This commits to full-clear-plus-full-redraw, which the triage flagged as the simpler and more obviously correct starting point:

  • UIElementBase.Render() draws unconditionally for a visible element. Skipping a clean element would not preserve its previous output — the clear had already erased it.
  • IsDirty and Invalidate() stay as the "changed since last draw" signal and keep raising Invalidated; they no longer gate drawing. That is the plumbing a dirty-region renderer would build on later, once the per-pass clear is removed and real region tracking exists.
  • Docs updated to match: the Render/Invalidate remarks, the IUIElement.Invalidate() contract, and the rendering-flow section of CLAUDE.md, which still described the old dirty-only model.

Deliberately out of scope: dirty-region rendering (the larger end-state noted in the issue), and #111's resize handling, which the triage sequenced behind this decision and which is now a small addition on top of it.

Tests

New TUI.Test/UIApplicationTests.cs (the file existed but was empty), using the repo's existing RecordingConsoleProvider double:

  • RenderAfterOnlyOneChildInvalidatesStillDrawsTheStaticSibling — the issue's exact scenario: a titled border around a static label and an interactive sibling; invalidate only the interactive child and assert the static label, the sibling, and the border title are all drawn in the next pass.
  • RenderCleanElementTwiceDrawsItTwice — a clean element redraws instead of being skipped.
  • RenderRedrawsTheWholeTreeOnEveryClear — clears and full-tree redraws stay in step across three passes.
  • RenderAssignsConsoleDimensionsToAnUnsizedRoot — guards the existing first-pass sizing behaviour (passes before and after the change).

Verification:

  • The three behavioural tests fail against main's UIElementBase.Render() (confirmed by temporarily restoring the IsDirty gate: 3 of 4 failed) and pass with the change.
  • dotnet build TUI.sln — clean across net8.0/net9.0/net10.0, 0 warnings.
  • dotnet test — 117/117 pass, up from 113 baseline, no regressions.

🤖 Generated with Claude Code

https://claude.ai/code/session_017nBCLgjFXPejQfEDEWyZkp


Generated by Claude Code

UIApplication.Render() clears the entire console on every pass, but
UIElementBase.Render() only drew while IsDirty was still set, and IsDirty
was cleared after an element's first draw. The two mechanisms cancelled
out: the screen was wiped each frame while only the elements the last
input had invalidated were redrawn, so any static element vanished on the
very next pass — a label beside a focused input disappeared on the first
keystroke.

Commit to one rendering model. A visible element now draws unconditionally,
so every clear is followed by a full redraw. IsDirty and Invalidate() stay
as the "changed since last draw" signal rather than a drawing gate, which
is what a future dirty-region renderer would build on once the clear is
removed and real region tracking exists.

Adds UIApplicationTests covering the issue's scenario (invalidate one child,
assert its static sibling and the border title are still drawn), that clears
and full redraws stay in step across passes, and that an unsized root still
takes its size from the console.

Fixes #109

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nBCLgjFXPejQfEDEWyZkp
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit bbceecc into main Sep 15, 2026
13 checks passed
@matt-edmondson
matt-edmondson deleted the claude/nice-davinci-fufgxs branch September 15, 2026 22:34
matt-edmondson pushed a commit that referenced this pull request Sep 16, 2026
#113 (for #109) and #118 (for #114) landed on main while this was open.
UIApplication.cs merged cleanly: #113 rewrote Render's dirty-tracking model,
which this branch does not touch, and this branch's changes are confined to
RunAsync's interrupt registration and ProcessInputAsync's read.

UIApplicationTests.cs conflicted because both branches filled what had been an
empty file, with two unrelated sets of tests. Neither side is redundant, so
main's render-loop tests stay in UIApplicationTests.cs unchanged and this
branch's run-lifecycle tests move to UIApplicationLifecycleTests.cs. That
matches how the suite already splits BorderElementTests from
BorderElementRenderTests, and keeps either class readable on its own.

129/129 tests pass and the solution builds clean across net10.0;net9.0;net8.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01671tcpA4zkfbgPcJm8cTsB
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.

Static (non-dirty) elements vanish from screen after the first redraw because full-screen Clear() runs every frame

1 participant