Skip to content

Commit 2a12625

Browse files
authored
Merge pull request #5464 from harder/copilot/issue-5434-adornment-layout-deltas
Fixes #5434. Track adornment thickness deltas so LayoutAndDraw can stop force-drawing
2 parents 29176d6 + 61aaa71 commit 2a12625

3 files changed

Lines changed: 82 additions & 35 deletions

File tree

Terminal.Gui/App/ApplicationImpl.Screen.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,18 +278,16 @@ public void LayoutAndDraw (bool forceRedraw = false)
278278
Rectangle clipRect = Screen with { X = 0, Y = 0 };
279279
Driver.Clip = new Region (clipRect);
280280

281-
// Only force a complete redraw if needed (needsLayout or forceRedraw).
282-
// Otherwise, just redraw views that need it.
281+
// Only force a complete redraw when explicitly requested.
282+
// Otherwise, redraw only the views that need it.
283283
//
284284
// NOTE (#5358): passing force=true here calls SetNeedsDraw on the top runnable, which
285285
// cascades to all overlapping subviews via the existing SetNeedsDraw recursion. This
286286
// is the remaining draw-fan-out source documented by TabsFanOutIntegrationTests. The
287287
// proper fix requires tracking adornment thickness changes (in addition to Frame
288288
// changes) so the SuperView can be invalidated precisely instead of force-redrawing
289-
// the whole tree. Dropping force-on-neededLayout without that tracking exposes
290-
// stale-content bugs in the shrink/move and adornment-rebalance paths (covered by
291-
// ShadowTests / BorderViewTests).
292-
View.Draw (views.ToArray ().Cast<View> (), neededLayout || forceRedraw);
289+
// the whole tree.
290+
View.Draw (views.ToArray ().Cast<View> (), forceRedraw);
293291

294292
Driver.Clip = new Region (clipRect);
295293

Terminal.Gui/ViewBase/View.Adornments.cs

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,54 @@ private void SetupAdornments ()
1111
{
1212
return;
1313
}
14+
15+
Thickness previousMarginThickness = Margin.Thickness;
16+
Thickness previousBorderThickness = Border.Thickness;
17+
Thickness previousPaddingThickness = Padding.Thickness;
18+
1419
Margin.Parent = this;
1520
Border.Parent = this;
1621
Padding.Parent = this;
1722

1823
// When any adornment's thickness changes, recompute frames and request layout + redraw.
1924
Margin.ThicknessChanged += (_, _) =>
2025
{
21-
Margin.View?.SetNeedsLayout ();
22-
SetAdornmentFrames ();
23-
SetNeedsLayout ();
24-
SetNeedsDraw ();
26+
previousMarginThickness = HandleAdornmentThicknessChanged (Margin, previousMarginThickness);
2527
};
2628

2729
Border.ThicknessChanged += (_, _) =>
2830
{
29-
Border.View?.SetNeedsLayout ();
30-
SetAdornmentFrames ();
31-
SetNeedsLayout ();
32-
SetNeedsDraw ();
31+
previousBorderThickness = HandleAdornmentThicknessChanged (Border, previousBorderThickness);
3332
};
3433

3534
Padding.ThicknessChanged += (_, _) =>
3635
{
37-
Padding.View?.SetNeedsLayout ();
38-
SetAdornmentFrames ();
39-
SetNeedsLayout ();
40-
SetNeedsDraw ();
36+
previousPaddingThickness = HandleAdornmentThicknessChanged (Padding, previousPaddingThickness);
4137
};
38+
39+
Thickness HandleAdornmentThicknessChanged (IAdornment adornment, Thickness previousThickness)
40+
{
41+
Rectangle oldViewport = GetViewportForAdornmentThickness (adornment, previousThickness);
42+
Rectangle oldViewportFrame = GetViewportFrameForAdornmentThickness (adornment, previousThickness);
43+
Thickness currentThickness = adornment.Thickness;
44+
45+
(adornment.View as View)?.SetNeedsLayout ();
46+
SetAdornmentFrames ();
47+
SetNeedsLayout ();
48+
SetNeedsDraw ();
49+
50+
if (oldViewportFrame != GetViewportFrameForAdornmentThickness (adornment, currentThickness))
51+
{
52+
InvalidateAdornmentThicknessChange ();
53+
}
54+
55+
if (oldViewport != Viewport)
56+
{
57+
RaiseViewportChangedEvent (oldViewport);
58+
}
59+
60+
return currentThickness;
61+
}
4262
}
4363

4464
private void BeginInitAdornments ()
@@ -345,4 +365,47 @@ internal void SetAdornmentFrames ()
345365
// Border and Padding dynamically update based on Margin's View's Frame changing
346366
Margin.View?.Frame = Frame with { Location = Point.Empty };
347367
}
368+
369+
private Rectangle GetViewportForAdornmentThickness (IAdornment changedAdornment, Thickness changedThickness)
370+
{
371+
Thickness thickness = GetAdornmentsThicknessForAdornment (changedAdornment, changedThickness);
372+
373+
return new Rectangle (_viewportLocation, GetViewportSizeForThickness (thickness));
374+
}
375+
376+
private Rectangle GetViewportFrameForAdornmentThickness (IAdornment changedAdornment, Thickness changedThickness)
377+
{
378+
Thickness thickness = GetAdornmentsThicknessForAdornment (changedAdornment, changedThickness);
379+
380+
return new Rectangle (new Point (thickness.Left, thickness.Top), GetViewportSizeForThickness (thickness));
381+
}
382+
383+
private Size GetViewportSizeForThickness (Thickness thickness) =>
384+
new (Math.Max (0, Frame.Width - thickness.Horizontal), Math.Max (0, Frame.Height - thickness.Vertical));
385+
386+
private Thickness GetAdornmentsThicknessForAdornment (IAdornment changedAdornment, Thickness changedThickness)
387+
{
388+
Thickness thickness = Thickness.Empty;
389+
390+
thickness += ReferenceEquals (changedAdornment, Margin) ? changedThickness : Margin.Thickness;
391+
thickness += ReferenceEquals (changedAdornment, Border) ? changedThickness : Border.Thickness;
392+
thickness += ReferenceEquals (changedAdornment, Padding) ? changedThickness : Padding.Thickness;
393+
394+
return thickness;
395+
}
396+
397+
private void InvalidateAdornmentThicknessChange ()
398+
{
399+
if (SuperView is null)
400+
{
401+
NeedsClearScreenNextIteration ();
402+
403+
return;
404+
}
405+
406+
Rectangle invalidationViewport = Frame;
407+
Point superScroll = SuperView.Viewport.Location;
408+
invalidationViewport.Offset (-superScroll.X, -superScroll.Y);
409+
SuperView.SetNeedsDraw (invalidationViewport);
410+
}
348411
}

Tests/IntegrationTests/TabsFanOutIntegrationTests.cs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ private static string MakeText (string prefix, int lines)
6868
}
6969

7070
/// <summary>
71-
/// End-to-end check: a real <see cref="Key.PageDown"/> on the active tab still produces
72-
/// draw fan-out on inactive tabs, but layout work stays on the active tab.
71+
/// End-to-end check: a real <see cref="Key.PageDown"/> on the active tab keeps both
72+
/// layout and text drawing on the active tab.
7373
/// </summary>
7474
[Theory]
7575
[MemberData (nameof (GetAllDriverNames))]
@@ -169,21 +169,7 @@ public void Integration_RealPageDown_OnActiveTab_DoesNotFanOutLayoutToInactiveTa
169169
outputHelper.WriteLine ($"Sum inactive DrawingText = {inactiveTextDraws}");
170170
outputHelper.WriteLine ($"Sum inactive SubViewsLaidOut = {inactiveLayouts}");
171171

172-
// Issue #5358 fix narrows draw fan-out at the View.Draw pipeline level (verified by
173-
// TabsFanOutDiagnosticTests at synthetic level). At integration level a separate
174-
// cascade source remains: ApplicationImpl.LayoutAndDraw passes force=true to
175-
// View.Draw whenever any view needed layout, which calls SetNeedsDraw on the top
176-
// runnable, which cascades to overlapping subviews via the existing SetNeedsDraw
177-
// recursion. Removing that force=true uncovers stale-content bugs in the shrink/move
178-
// path (covered by existing ShadowTests and BorderViewTests) and is out of scope
179-
// for #5358. Until that broader fix lands, inactive tab pages still receive
180-
// NeedsDraw via the LayoutAndDraw force path. Layout fan-out is already fully
181-
// eliminated by PR #5373.
182-
Assert.True (
183-
inactiveTextDraws > 0,
184-
$"Documents the remaining draw fan-out via ApplicationImpl.LayoutAndDraw's force=true path: " +
185-
$"inactive_total DrawingText={inactiveTextDraws}. Flip to Assert.Equal(0, inactiveTextDraws) " +
186-
"after the broader LayoutAndDraw cascade is addressed (out of scope for #5358).");
172+
Assert.Equal (0, inactiveTextDraws);
187173

188174
Assert.Equal (0, inactiveLayouts);
189175
}

0 commit comments

Comments
 (0)