diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index d7b4bf3c35..8283b7c6d3 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -137,18 +137,18 @@ public void Draw (DrawContext? context = null) DoDrawContent (context); // ------------------------------------ - // Draw the line canvas - // Restore the clip before rendering the line canvas and adornment subviews - // because they may draw outside the viewport. + // Draw adornment SubViews BEFORE rendering LineCanvas so their lines + // (merged via LineCanvas.Merge) participate in auto-join. + // Restore the clip because adornment subviews may draw outside the viewport. SetClip (originalClip); originalClip = AddFrameToClip (); - Trace.Draw (this.ToIdentifyingString (), "LineCanvas"); - DoRenderLineCanvas (context); + Trace.Draw (this.ToIdentifyingString (), "AdornmentSubViews"); + DoDrawAdornmentsSubViews (context); // ------------------------------------ - // Re-draw the Border and Padding Adornment SubViews - // HACK: This is a hack to ensure that the Border and Padding Adornment SubViews are drawn after the line canvas. - DoDrawAdornmentsSubViews (context); + // Draw the line canvas (includes merged lines from adornment SubViews) + Trace.Draw (this.ToIdentifyingString (), "LineCanvas"); + DoRenderLineCanvas (context); // ------------------------------------ // Advance the diagnostics draw indicator @@ -208,7 +208,13 @@ private void DoDrawAdornmentsSubViews (DrawContext? context) subview.SetNeedsDraw (); } - LineCanvas.Exclude (new Region (subview.FrameToScreen ())); + // Only Exclude SubViews that don't merge their LC into the parent. + // SuperViewRendersLineCanvas SubViews contribute LC lines via Merge, and + // excluding them would prevent those merged lines from rendering. + if (!subview.SuperViewRendersLineCanvas) + { + LineCanvas.Exclude (new Region (subview.FrameToScreen ())); + } } Region? saved = borderView.AddFrameToClip (); @@ -750,6 +756,7 @@ public void DrawSubViews (DrawContext? context = null) { continue; } + LineCanvas.Merge (view.LineCanvas); view.LineCanvas.Clear (); } diff --git a/Terminal.Gui/ViewBase/View.NeedsDraw.cs b/Terminal.Gui/ViewBase/View.NeedsDraw.cs index b3e44435a1..fc2e123b5d 100644 --- a/Terminal.Gui/ViewBase/View.NeedsDraw.cs +++ b/Terminal.Gui/ViewBase/View.NeedsDraw.cs @@ -144,8 +144,11 @@ internal void ClearNeedsDraw () SubViewNeedsDraw = false; - // This ensures LineCanvas' get redrawn - if (!SuperViewRendersLineCanvas) + // This ensures LineCanvas' get redrawn. + // AdornmentViews skip this because their LC may hold merged SubView lines + // that haven't been consumed by the parent's DoDrawAdornmentsSubViews yet. + // Those lines are cleared in DoDrawAdornmentsSubViews after merging into the parent's LC. + if (!SuperViewRendersLineCanvas && this is not AdornmentView) { LineCanvas.Clear (); } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/AdornmentSubViewLineCanvasTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/AdornmentSubViewLineCanvasTests.cs new file mode 100644 index 0000000000..a11943ef10 --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/AdornmentSubViewLineCanvasTests.cs @@ -0,0 +1,78 @@ +// Copilot +using Terminal.Gui.Tracing; +using UnitTests; + +namespace ViewBaseTests.Draw; + +/// +/// Tests that SubViews of adornments with = true +/// get their border lines auto-joined with the parent View's border lines. +/// +/// BUG (#4854): runs AFTER +/// in the draw pipeline, so merged lines arrive too late. +/// +public class AdornmentSubViewLineCanvasTests (ITestOutputHelper output) : TestDriverBase +{ + /// + /// Simplest repro: A 7×3 View with only a top border line. A SubView in the Border + /// adds a double-line segment via SuperViewRendersLineCanvas. The ═ should appear + /// but doesn't because the merge happens after the LineCanvas was already rendered. + /// + [Fact] + public void BorderSubView_Lines_Not_Rendered () + { + using IDisposable tracing = TestLogging.Verbose (output, TraceCategory.Draw); + + IDriver driver = CreateTestDriver (7, 3); + driver.Clip = new Region (driver.Screen); + + View parent = new () + { + Id = "parent", + Driver = driver, + Width = 7, + Height = 3, + BorderStyle = LineStyle.Single + }; + parent.Border.Thickness = new Thickness (0, 1, 0, 0); + + View sub = new () + { + Id = "sub", + X = 2, + Y = 0, + Width = 3, + Height = 1, + SuperViewRendersLineCanvas = true + }; + parent.Border.GetOrCreateView ().Add (sub); + + parent.BeginInit (); + parent.EndInit (); + parent.Layout (); + + Rectangle subScreen = sub.FrameToScreen (); + output.WriteLine ($"subScreen: {subScreen}"); + + sub.LineCanvas.AddLine ( + new Point (subScreen.X, subScreen.Y), + 3, + Orientation.Horizontal, + LineStyle.Double); + + output.WriteLine ($"sub.LC.Bounds before Draw: {sub.LineCanvas.Bounds}"); + + parent.Draw (); + + output.WriteLine ($"Driver output:"); + output.WriteLine (driver.ToString ()); + + // Expected: ──═══── (double-line from SubView merged with parent's single-line) + DriverAssert.AssertDriverContentsAre ( + """ + ──═══── + """, + output, + driver); + } +}