-
Notifications
You must be signed in to change notification settings - Fork 774
Fixes #5357 - Avoid redraw fan-out from ancestor-only layout propagation #5373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
harder
merged 16 commits into
gui-cs:develop
from
harder:fix-5357-split-layout-propagation
May 26, 2026
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
78c76de
Fixes #5356. Add tab fan-out layout/draw diagnostic tests
harder 60b8459
Swap TextView for Code; add integration test for fan-out
harder 7e3ccb6
Potential fix for pull request finding
harder 09017a9
Add tests for #5357 ancestor-only layout propagation
harder cd8c2f0
Fixes #5357. Split upward layout propagation from downward subtree in…
harder 1fb219b
Fixes #5357. Avoid redraw fan-out from ancestor-only layout propagation
harder d4df487
Merge pull request #5364 from harder/fix-5356-tab-fanout-diagnostics
harder 57c0f3e
Add tests for #5357 ancestor-only layout propagation
harder 65154a4
Fixes #5357. Split upward layout propagation from downward subtree in…
harder 7be7ed0
Fixes #5357. Avoid redraw fan-out from ancestor-only layout propagation
harder 8b4f7b9
Merge remote-tracking branch 'origin/fix-5357-split-layout-propagatio…
harder 5aa5d51
Fix possessive typo in SetNeedsLayout doc comment
Copilot 2775087
Fix doc comment grammar in Layout return description
Copilot 1489e75
Fixes #5357. Update fan-out tests for layout-only fix
harder b231e5a
Fix review comments: update AI headers to // Copilot and fix #5356 → …
tig 00db601
Fixes #5357. Narrow SetNeedsLayout propagation
harder File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| using System.Text; | ||
| using AppTestHelpers; | ||
|
|
||
| namespace IntegrationTests; | ||
|
|
||
| // Copilot | ||
|
|
||
| /// <summary> | ||
| /// Integration counterpart to <c>TabsFanOutDiagnosticTests</c>. Drives the active tab via real | ||
| /// key injection through the driver's input processor → command dispatch → main-loop | ||
| /// <c>LayoutAndDraw</c> path, instead of mutating <see cref="View.Viewport"/> directly. This | ||
| /// verifies the fan-out from issue #4973 / #5357 is observable end-to-end, not just under | ||
| /// synthetic <see cref="View.Layout()"/> / <see cref="View.Draw"/> calls. | ||
| /// </summary> | ||
|
harder marked this conversation as resolved.
|
||
| /// <remarks> | ||
| /// Instrumentation-only. The per-tab counters are attached to event subscriptions on | ||
| /// <see cref="View.DrawComplete"/>, <see cref="View.SubViewsLaidOut"/>, and | ||
| /// <see cref="View.ClearedViewport"/>; no rendering or invalidation behavior is changed. | ||
| /// </remarks> | ||
| public class TabsFanOutIntegrationTests (ITestOutputHelper outputHelper) : TestsAllDrivers | ||
| { | ||
| private readonly TextWriter _out = new TestOutputWriter (outputHelper); | ||
|
|
||
| /// <summary> | ||
| /// A <see cref="Code"/> that registers <see cref="Command.ScrollDown"/> / | ||
| /// <see cref="Command.ScrollUp"/> so <see cref="Key.PageDown"/> / <see cref="Key.PageUp"/> | ||
| /// drive vertical scrolling through the normal command pipeline. Used only by this test — | ||
| /// <see cref="Code"/> doesn't expose <c>AddCommand</c> publicly, so a subclass is the | ||
| /// simplest way to wire a real input → scroll path without modifying production code. | ||
| /// </summary> | ||
| private sealed class ScrollableCode : Code | ||
| { | ||
| public ScrollableCode () | ||
| { | ||
| AddCommand (Command.ScrollDown, () => ScrollVertical (1)); | ||
| AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); | ||
|
|
||
| KeyBindings.Add (Key.PageDown, Command.ScrollDown); | ||
| KeyBindings.Add (Key.PageUp, Command.ScrollUp); | ||
| } | ||
| } | ||
|
|
||
| private sealed class Counters | ||
| { | ||
| public int SubViewsLaidOut; | ||
| public int DrawComplete; | ||
| public int ClearedViewport; | ||
| } | ||
|
|
||
| private static string MakeText (string prefix, int lines) | ||
| { | ||
| StringBuilder sb = new (); | ||
|
|
||
| for (var i = 1; i <= lines; i++) | ||
| { | ||
| sb.Append (prefix); | ||
| sb.Append (' '); | ||
| sb.Append (i); | ||
|
|
||
| if (i < lines) | ||
| { | ||
| sb.Append ('\n'); | ||
| } | ||
| } | ||
|
|
||
| return sb.ToString (); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// End-to-end check: a real <see cref="Key.PageDown"/> on the active tab still produces | ||
| /// draw fan-out on inactive tabs, but layout work stays on the active tab. | ||
| /// </summary> | ||
| [Theory] | ||
| [MemberData (nameof (GetAllDriverNames))] | ||
| public void Integration_RealPageDown_OnActiveTab_DoesNotFanOutLayoutToInactiveTabs (string driverName) | ||
| { | ||
| const int TabCount = 4; | ||
|
|
||
| Tabs tabs = new () { Width = Dim.Fill (), Height = Dim.Fill () }; | ||
| ScrollableCode [] codes = new ScrollableCode [TabCount]; | ||
|
|
||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| codes [i] = new ScrollableCode | ||
| { | ||
| Title = $"Tab{i + 1}", | ||
| Text = MakeText ($"Tab{i + 1} line", 80), | ||
| Language = null, | ||
| SyntaxHighlighter = null, | ||
| Width = Dim.Fill (), | ||
| Height = Dim.Fill () | ||
| }; | ||
|
|
||
| tabs.Add (codes [i]); | ||
| } | ||
|
|
||
| Counters [] perTab = new Counters [TabCount]; | ||
| Counters tabsContainer = new (); | ||
|
|
||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| int captured = i; | ||
| perTab [i] = new Counters (); | ||
| codes [i].SubViewsLaidOut += (_, _) => perTab [captured].SubViewsLaidOut++; | ||
| codes [i].DrawComplete += (_, _) => perTab [captured].DrawComplete++; | ||
| codes [i].ClearedViewport += (_, _) => perTab [captured].ClearedViewport++; | ||
| } | ||
|
|
||
| tabs.SubViewsLaidOut += (_, _) => tabsContainer.SubViewsLaidOut++; | ||
| tabs.DrawComplete += (_, _) => tabsContainer.DrawComplete++; | ||
| tabs.ClearedViewport += (_, _) => tabsContainer.ClearedViewport++; | ||
|
|
||
| ScrollableCode active = codes [0]; | ||
|
|
||
| using AppTestHelper helper = With.A<Window> (60, 20, driverName, _out) | ||
| .Add (tabs) | ||
| .Focus (active) | ||
| .Then ( | ||
| _ => | ||
| { | ||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| perTab [i].SubViewsLaidOut = 0; | ||
| perTab [i].DrawComplete = 0; | ||
| perTab [i].ClearedViewport = 0; | ||
| } | ||
|
|
||
| tabsContainer.SubViewsLaidOut = 0; | ||
| tabsContainer.DrawComplete = 0; | ||
| tabsContainer.ClearedViewport = 0; | ||
| }) | ||
| .KeyDown (Key.PageDown) | ||
| .KeyDown (Key.PageDown) | ||
| .KeyDown (Key.PageDown); | ||
|
|
||
| outputHelper.WriteLine ($"Driver: {driverName}"); | ||
| outputHelper.WriteLine ($"Active tab viewport Y after 3 PageDowns: {active.Viewport.Y}"); | ||
| outputHelper.WriteLine ("Per-tab counters (after 3 PageDowns on active tab):"); | ||
| outputHelper.WriteLine (" tab laidOut drawComplete clearedViewport"); | ||
| outputHelper.WriteLine ($" Tabs {tabsContainer.SubViewsLaidOut,7} {tabsContainer.DrawComplete,12} {tabsContainer.ClearedViewport,15}"); | ||
|
|
||
| for (var i = 0; i < TabCount; i++) | ||
| { | ||
| outputHelper.WriteLine ($" Code{i + 1,-6} {perTab [i].SubViewsLaidOut,7} {perTab [i].DrawComplete,12} {perTab [i].ClearedViewport,15}"); | ||
| } | ||
|
|
||
| Assert.True ( | ||
| active.Viewport.Y > 0, | ||
| $"PageDown should have scrolled the active tab via real input → command path. Got Viewport.Y={active.Viewport.Y}."); | ||
|
|
||
| Assert.True ( | ||
| perTab [0].DrawComplete > 0, | ||
| $"Active tab must draw in response to real PageDown, got DrawComplete={perTab [0].DrawComplete}."); | ||
|
|
||
| int inactiveDraws = 0; | ||
| int inactiveLayouts = 0; | ||
|
|
||
| for (var i = 1; i < TabCount; i++) | ||
| { | ||
| inactiveDraws += perTab [i].DrawComplete; | ||
| inactiveLayouts += perTab [i].SubViewsLaidOut; | ||
| } | ||
|
|
||
| outputHelper.WriteLine ($"Sum inactive DrawComplete = {inactiveDraws}"); | ||
| outputHelper.WriteLine ($"Sum inactive SubViewsLaidOut = {inactiveLayouts}"); | ||
|
|
||
| // CURRENT BEHAVIOR: draw fan-out still exists through the real input → command → main-loop path, | ||
| // but layout fan-out should now be eliminated. | ||
| Assert.True ( | ||
| inactiveDraws > 0, | ||
| $"Documents issue #4973 (integration-level): inactive_total DrawComplete={inactiveDraws}. " + | ||
| "Flip to Assert.Equal(0, inactiveDraws) after fix lands."); | ||
|
|
||
| Assert.Equal ( | ||
| 0, | ||
| inactiveLayouts); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.