diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
index ed3e529ec4..7093985121 100644
--- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
+++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
@@ -394,10 +394,124 @@ public void Merge (LineCanvas lineCanvas)
AddLine (line);
}
- if (lineCanvas._exclusionRegion is { })
+ if (lineCanvas._exclusionRegion is null)
{
- _exclusionRegion ??= new ();
- _exclusionRegion.Union (lineCanvas._exclusionRegion);
+ return;
+ }
+
+ _exclusionRegion ??= new Region ();
+ _exclusionRegion.Union (lineCanvas._exclusionRegion);
+ }
+
+ ///
+ /// Merges one line canvas into this one, excluding lines (or portions of lines) that fall
+ /// within . Lines that partially overlap the exclusion region are
+ /// split into segments that skip the excluded cells. The exclusion is applied at the line level
+ /// so that excluded cells do not participate in auto-join intersection resolution.
+ ///
+ /// The source canvas to merge from.
+ ///
+ /// The region to exclude. Cells of incoming lines that fall within this region will not be merged.
+ /// Pass for default merge behavior (no exclusion).
+ ///
+ public void Merge (LineCanvas lineCanvas, Region? exclude)
+ {
+ if (exclude is null || exclude.IsEmpty ())
+ {
+ Merge (lineCanvas);
+
+ return;
+ }
+
+ Rectangle excludeBounds = exclude.GetBounds ();
+ Rectangle [] excludeRects = exclude.GetRectangles ();
+
+ foreach (StraightLine line in lineCanvas._lines)
+ {
+ AddLineExcluding (line, excludeBounds, excludeRects);
+ }
+
+ if (lineCanvas._exclusionRegion is null)
+ {
+ return;
+ }
+
+ _exclusionRegion ??= new Region ();
+ _exclusionRegion.Union (lineCanvas._exclusionRegion);
+
+ return;
+
+ // Adds segments of `line` that do not overlap with the exclusion rectangles.
+ void AddLineExcluding (StraightLine line, Rectangle exBounds, Rectangle [] exRects)
+ {
+ Rectangle bounds = line.Bounds;
+
+ // Fast path: if the line doesn't intersect the exclusion bounds at all, add it whole.
+ if (!bounds.IntersectsWith (exBounds))
+ {
+ AddLine (line);
+
+ return;
+ }
+
+ // Walk cells along the line's axis, building non-excluded segments.
+ bool isHorizontal = line.Orientation == Orientation.Horizontal;
+ int axisStart = isHorizontal ? bounds.X : bounds.Y;
+ int axisEnd = axisStart + (isHorizontal ? bounds.Width : bounds.Height);
+ int fixedCoord = isHorizontal ? bounds.Y : bounds.X;
+
+ int segStart = -1;
+
+ for (int i = axisStart; i < axisEnd; i++)
+ {
+ int x = isHorizontal ? i : fixedCoord;
+ int y = isHorizontal ? fixedCoord : i;
+
+ if (!ContainedInAny (exRects, x, y))
+ {
+ if (segStart < 0)
+ {
+ segStart = i;
+ }
+
+ continue;
+ }
+
+ // Cell is excluded — flush any pending segment.
+ if (segStart < 0)
+ {
+ continue;
+ }
+
+ EmitSegment (line, isHorizontal, fixedCoord, segStart, i - segStart);
+ segStart = -1;
+ }
+
+ // Flush trailing segment.
+ if (segStart >= 0)
+ {
+ EmitSegment (line, isHorizontal, fixedCoord, segStart, axisEnd - segStart);
+ }
+ }
+
+ static bool ContainedInAny (Rectangle [] rects, int x, int y)
+ {
+ for (var i = 0; i < rects.Length; i++)
+ {
+ if (rects [i].Contains (x, y))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ void EmitSegment (StraightLine original, bool isHorizontal, int fixedCoord, int segAxisStart, int segLength)
+ {
+ Point start = isHorizontal ? new Point (segAxisStart, fixedCoord) : new Point (fixedCoord, segAxisStart);
+
+ AddLine (new StraightLine (start, segLength, original.Orientation, original.Style, original.Attribute));
}
}
diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs
index 189ae57dea..1b5a0cc976 100644
--- a/Terminal.Gui/ViewBase/Adornment/Border.cs
+++ b/Terminal.Gui/ViewBase/Adornment/Border.cs
@@ -25,10 +25,10 @@ protected override AdornmentView CreateView ()
return bv;
}
- ///
+ ///
public override Rectangle GetFrame () => Parent is { } ? Parent.Margin.Thickness.GetInside (Parent!.Margin.GetFrame ()) : Rectangle.Empty;
- ///
+ ///
protected override void OnThicknessChanged ()
{
base.OnThicknessChanged ();
@@ -48,7 +48,8 @@ protected override void OnThicknessChanged ()
///
/// Sets the style of the lines drawn in the . If not set, will inherit the style from
- /// the 's 's . If set, will cause
+ /// the 's 's . If set, will
+ /// cause
/// to be created.
///
public LineStyle? LineStyle
@@ -63,7 +64,7 @@ public LineStyle? LineStyle
field = value;
- if (field is not null)
+ if (field is { })
{
GetOrCreateView ();
}
@@ -82,9 +83,87 @@ public BorderSettings Settings
{
return;
}
+
field = value;
+ if (field.HasFlag (BorderSettings.Tab))
+ {
+ GetOrCreateView ();
+ }
+
+ SettingsChanged?.Invoke (this, EventArgs.Empty);
Parent?.SetNeedsLayout ();
}
} = BorderSettings.Title;
+
+ /// Fired when changes.
+ public event EventHandler? SettingsChanged;
+
+ ///
+ /// Gets or sets which side the Tab protrudes from. Only used when is set.
+ ///
+ public Side TabSide
+ {
+ get;
+ set
+ {
+ if (field == value)
+ {
+ return;
+ }
+
+ field = value;
+ Parent?.SetNeedsLayout ();
+ }
+ } = Side.Top;
+
+ ///
+ /// Gets or sets the offset along the border edge where the Tab starts (columns for Top/Bottom,
+ /// rows for Left/Right). Only used when is set.
+ ///
+ public int TabOffset
+ {
+ get;
+ set
+ {
+ if (field == value)
+ {
+ return;
+ }
+
+ field = value;
+ Parent?.SetNeedsLayout ();
+ }
+ }
+
+ ///
+ /// Gets or sets the total length of the tab parallel to the border edge (including border cells).
+ /// If the length will be determined based on .
+ /// Only used when is set.
+ ///
+ public int? TabLength
+ {
+ get
+ {
+ if (field is { } || !Settings.HasFlag (BorderSettings.Tab))
+ {
+ return field;
+ }
+
+ int titleColumns = Settings.HasFlag (BorderSettings.Title) ? Parent?.TitleTextFormatter.FormatAndGetSize ().Width ?? 0 : 0;
+
+ // Two vertical border lines + title text width (2 when no title)
+ return titleColumns + 2;
+ }
+ set
+ {
+ if (field == value)
+ {
+ return;
+ }
+
+ field = value;
+ Parent?.SetNeedsLayout ();
+ }
+ }
}
diff --git a/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs b/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs
index 4921446aec..0acc9bd481 100644
--- a/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs
+++ b/Terminal.Gui/ViewBase/Adornment/BorderSettings.cs
@@ -20,4 +20,12 @@ public enum BorderSettings
/// Use to draw the border.
///
Gradient = 2,
+
+ ///
+ /// Draw a Tab on one side of the border. The will be displayed in the Tab. Configure with
+ /// , ,
+ /// .
+ ///
+ Tab = 4,
}
+
diff --git a/Terminal.Gui/ViewBase/Adornment/BorderView.cs b/Terminal.Gui/ViewBase/Adornment/BorderView.cs
index 423d3586c5..fc9c4fa70f 100644
--- a/Terminal.Gui/ViewBase/Adornment/BorderView.cs
+++ b/Terminal.Gui/ViewBase/Adornment/BorderView.cs
@@ -61,6 +61,7 @@ public BorderView (Border border) : base (border)
}
border.ThicknessChanged += OnThicknessChanged;
border.Parent?.Margin.ThicknessChanged += OnThicknessChanged;
+ border.SettingsChanged += OnSettingsChanged;
}
///
@@ -84,6 +85,122 @@ private void OnThicknessChanged (object? sender, EventArgs e)
{
ShowHideDrawIndicator ();
}
+
+ ConfigureForTabMode ();
+ }
+
+ private void OnSettingsChanged (object? sender, EventArgs e) => ConfigureForTabMode ();
+
+ private bool _tabModeSetTransparent;
+
+ ///
+ /// Configures persistent state for tab mode. Called when or
+ /// changes. Sets and
+ /// ensures the SubView exists with the correct static properties.
+ ///
+ private void ConfigureForTabMode ()
+ {
+ if (Adornment is not Border border)
+ {
+ return;
+ }
+
+ if (border.Settings.FastHasFlags (BorderSettings.Tab))
+ {
+ ViewportSettings |= ViewportSettingsFlags.Transparent | ViewportSettingsFlags.TransparentMouse;
+ _tabModeSetTransparent = true;
+
+ TabTitleView label = EnsureTabTitleView ();
+
+ if (border.LineStyle is { } ls)
+ {
+ label.BorderStyle = ls;
+ }
+
+ label.TextFormatter.Direction = border.TabSide is Side.Left or Side.Right ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
+ }
+ else
+ {
+ // Only clear flags if we set them for tab mode
+ if (_tabModeSetTransparent)
+ {
+ ViewportSettings &= ~(ViewportSettingsFlags.Transparent | ViewportSettingsFlags.TransparentMouse);
+ _tabModeSetTransparent = false;
+ }
+
+ if (_tabTitleView is { })
+ {
+ _tabTitleView.Visible = false;
+ }
+ }
+ }
+
+ ///
+ protected override void OnSubViewLayout (LayoutEventArgs args) => UpdateTabTitleViewLayout ();
+
+ ///
+ /// Computes and sets the 's frame, size, border thickness,
+ /// text, and visibility during the layout pass. Called via .
+ ///
+ private void UpdateTabTitleViewLayout ()
+ {
+ if (Adornment is not Border border || !border.Settings.FastHasFlags (BorderSettings.Tab))
+ {
+ return;
+ }
+
+ if (_tabTitleView is null)
+ {
+ return;
+ }
+
+ Rectangle borderBounds = GetTabBorderBounds (border);
+
+ if (borderBounds is not { Width: > 0, Height: > 0 })
+ {
+ _tabTitleView.Visible = false;
+
+ return;
+ }
+
+ int tabDepth = GetTabDepth (border);
+ int tabLength = border.TabLength!.Value;
+ bool hasFocus = border.Parent?.HasFocus ?? false;
+
+ Rectangle headerRect = ComputeHeaderRect (borderBounds, border.TabSide, border.TabOffset, tabLength, tabDepth);
+ Rectangle viewBounds = ComputeViewBounds (borderBounds, border.TabSide, tabDepth);
+ Rectangle clipped = Rectangle.Intersect (headerRect, viewBounds);
+ bool tabVisible = !clipped.IsEmpty;
+
+ if (!tabVisible)
+ {
+ _tabTitleView.Visible = false;
+
+ return;
+ }
+
+ _tabTitleView.Visible = true;
+ _tabTitleView.HotKeySpecifier = Adornment.Parent?.HotKeySpecifier ?? default (Rune);
+ _tabTitleView.Text = Adornment.Parent?.Title ?? string.Empty;
+
+ if (border.LineStyle is { } ls)
+ {
+ _tabTitleView.BorderStyle = ls;
+ }
+
+ // Configure the label's border thickness based on depth and focus
+ Thickness labelBorderThickness = ComputeTabLabelThickness (border.TabSide, tabDepth, hasFocus);
+ _tabTitleView.Border.Thickness = labelBorderThickness;
+
+ // For Left/Right, render text vertically
+ _tabTitleView.TextFormatter.Direction = border.TabSide is Side.Left or Side.Right
+ ? TextDirection.TopBottom_LeftRight
+ : TextDirection.LeftRight_TopBottom;
+
+ // Convert header rect from screen to BorderView viewport coords
+ Point screenOrigin = ViewportToScreen (Point.Empty);
+ Rectangle labelFrame = headerRect with { X = headerRect.X - screenOrigin.X, Y = headerRect.Y - screenOrigin.Y };
+ _tabTitleView.Frame = labelFrame;
}
private void ShowHideDrawIndicator ()
@@ -125,15 +242,6 @@ internal void AdvanceDrawIndicator ()
DrawIndicator.Render ();
}
-#if SUBVIEW_BASED_BORDER
- private Line _left;
-
- ///
- /// The close button for the border. Set to , to to enable.
- ///
- public Button CloseButton { get; internal set; }
-#endif
-
///
public override void BeginInit ()
{
@@ -145,53 +253,11 @@ public override void BeginInit ()
}
ShowHideDrawIndicator ();
+ ConfigureForTabMode ();
MouseHighlightStates |= Adornment.Parent.Arrangement != ViewArrangement.Fixed ? MouseState.Pressed : MouseState.None;
-
-#if SUBVIEW_BASED_BORDER
- if (Adornment.Parent is { })
- {
- // Left
- _left = new ()
- {
- Orientation = Orientation.Vertical,
- };
- Add (_left);
-
- CloseButton = new Button ()
- {
- Text = "X",
- CanFocus = true,
- Visible = false,
- };
- CloseButton.Accept += (s, e) =>
- {
- e.Handled = Parent.InvokeCommand (Command.Quit) == true;
- };
- Add (CloseButton);
-
- LayoutStarted += OnLayoutStarted;
- }
-#endif
}
-#if SUBVIEW_BASED_BORDER
- private void OnLayoutStarted (object sender, LayoutEventArgs e)
- {
- _left.Border.LineStyle = LineStyle;
-
- _left.X = Adornment!.Thickness.Left - 1;
- _left.Y = Adornment!.Thickness.Top - 1;
- _left.Width = 1;
- _left.Height = Height;
-
- CloseButton.X = Pos.AnchorEnd (Adornment!.Thickness.Right / 2 + 1) -
- (Pos.Right (CloseButton) -
- Pos.Left (CloseButton));
- CloseButton.Y = 0;
-}
-#endif
-
private Rectangle GetBorderBounds ()
{
Rectangle screenRect = ViewportToScreen (Viewport);
@@ -206,6 +272,469 @@ private Rectangle GetBorderBounds ()
- Math.Max (0, Math.Max (0, Adornment!.Thickness.Top - 1) + Math.Max (0, Adornment!.Thickness.Bottom - 1))));
}
+ ///
+ /// Computes the content border rectangle when is set.
+ /// Non-title sides use the outer edge of the thickness. The title side uses thickness - 1
+ /// from the outer edge, leaving a tab header region between the outer edge and the content border line.
+ ///
+ private Rectangle GetTabBorderBounds (Border border)
+ {
+ Rectangle screenRect = ViewportToScreen (Viewport);
+
+ int left = screenRect.X;
+ int top = screenRect.Y;
+ int right = screenRect.Right;
+ int bottom = screenRect.Bottom;
+
+ // Title side: content border at thickness - 1 from outer edge
+ switch (border.TabSide)
+ {
+ case Side.Top:
+ top += Math.Max (0, Adornment!.Thickness.Top - 1);
+
+ break;
+
+ case Side.Bottom:
+ bottom -= Math.Max (0, Adornment!.Thickness.Bottom - 1);
+
+ break;
+
+ case Side.Left:
+ left += Math.Max (0, Adornment!.Thickness.Left - 1);
+
+ break;
+
+ case Side.Right:
+ right -= Math.Max (0, Adornment!.Thickness.Right - 1);
+
+ break;
+ }
+
+ return new Rectangle (left, top, Math.Max (0, right - left), Math.Max (0, bottom - top));
+ }
+
+ private TabTitleView? _tabTitleView;
+
+ /// Gets the tab title , or if not yet created.
+ public View? TabTitleView => _tabTitleView;
+
+ ///
+ /// Gets or lazily creates the SubView used to render the tab header.
+ /// The view has its own border with = true,
+ /// so its border lines auto-join with the View's content border via .
+ ///
+ private TabTitleView EnsureTabTitleView ()
+ {
+ if (_tabTitleView is { })
+ {
+ return _tabTitleView;
+ }
+
+ _tabTitleView = new TabTitleView
+ {
+#if DEBUG
+ Id = "TabTitleView",
+#endif
+ CanFocus = false,
+ TabStop = TabBehavior.NoStop,
+ SuperViewRendersLineCanvas = true,
+ OwnerView = Adornment?.Parent
+ };
+ _tabTitleView.Border.Settings = BorderSettings.None;
+ Add (_tabTitleView);
+
+ return _tabTitleView;
+ }
+
+ ///
+ /// Computes the unclipped header rectangle for the given side, offset, length, and depth. In content coordinates.
+ ///
+ private static Rectangle ComputeHeaderRect (Rectangle contentBorderRect, Side side, int offset, int length, int depth) =>
+ side switch
+ {
+ Side.Top => new Rectangle (contentBorderRect.X + offset, contentBorderRect.Y - (depth - 1), length, depth),
+ Side.Bottom => new Rectangle (contentBorderRect.X + offset, contentBorderRect.Bottom - 1, length, depth),
+ Side.Left => new Rectangle (contentBorderRect.X - (depth - 1), contentBorderRect.Y + offset, depth, length),
+ Side.Right => new Rectangle (contentBorderRect.Right - 1, contentBorderRect.Y + offset, depth, length),
+ _ => Rectangle.Empty
+ };
+
+ ///
+ /// Computes the full view bounds (content border + header protrusion area). In content coordinates.
+ ///
+ private static Rectangle ComputeViewBounds (Rectangle contentBorderRect, Side side, int depth) =>
+ side switch
+ {
+ Side.Top => contentBorderRect with { Y = contentBorderRect.Y - (depth - 1), Height = contentBorderRect.Height + (depth - 1) },
+ Side.Bottom => contentBorderRect with { Height = contentBorderRect.Height + (depth - 1) },
+ Side.Left => contentBorderRect with { X = contentBorderRect.X - (depth - 1), Width = contentBorderRect.Width + (depth - 1) },
+ Side.Right => contentBorderRect with { Width = contentBorderRect.Width + (depth - 1) },
+ _ => contentBorderRect
+ };
+
+ ///
+ /// Computes the for the tab title Label's border based on
+ /// depth, focus state, and which side the tab is on.
+ ///
+ ///
+ ///
+ /// "Cap" is the outward edge (away from content). "Content" is the inward edge (toward content area).
+ /// For depth ≥ 3, the content-side thickness toggles with focus to create the open gap / separator.
+ /// For depth < 3, no focus distinction in border lines.
+ ///
+ ///
+ private static Thickness ComputeTabLabelThickness (Side tabSide, int depth, bool hasFocus)
+ {
+ int cap = depth >= 2 ? 1 : 0;
+ int contentSide = depth >= 3 && !hasFocus ? 1 : 0;
+
+ return tabSide switch
+ {
+ Side.Top => new Thickness (1, cap, 1, contentSide),
+ Side.Bottom => new Thickness (1, contentSide, 1, cap),
+ Side.Left => new Thickness (cap, 1, contentSide, 1),
+ Side.Right => new Thickness (contentSide, 1, cap, 1),
+ _ => Thickness.Empty
+ };
+ }
+
+ private int GetTabDepth (Border border)
+ {
+ int thickness = border.TabSide switch
+ {
+ Side.Top => Adornment!.Thickness.Top,
+ Side.Bottom => Adornment!.Thickness.Bottom,
+ Side.Left => Adornment!.Thickness.Left,
+ Side.Right => Adornment!.Thickness.Right,
+ _ => 3
+ };
+
+ return Math.Min (thickness, 3);
+ }
+
+ ///
+ /// Draws the border and tab header when is set.
+ /// Uses a SubView with its own border and
+ /// = true for the tab header.
+ /// The TabTitleView's border lines auto-join with the content border via .
+ ///
+ private bool DrawTabBorder (Border border)
+ {
+ if (Adornment?.Parent is null || Driver is null)
+ {
+ return true;
+ }
+
+ Rectangle screenBounds = ViewportToScreen (Viewport);
+ Rectangle borderBounds = GetTabBorderBounds (border);
+
+ if (borderBounds is not { Width: > 0, Height: > 0 })
+ {
+ return true;
+ }
+
+ if (border.LineStyle is null or LineStyle.None)
+ {
+ return true;
+ }
+
+ Attribute normalAttribute = GetAttributeForRole (VisualRole.Normal);
+
+ if (MouseState.HasFlag (MouseState.Pressed))
+ {
+ normalAttribute = GetAttributeForRole (VisualRole.Highlight);
+ }
+
+ SetAttribute (normalAttribute);
+
+ LineCanvas? lc = Adornment.Parent?.LineCanvas;
+
+ if (lc is null)
+ {
+ return true;
+ }
+
+ int tabDepth = GetTabDepth (border);
+ int tabLength = border.TabLength!.Value;
+ LineStyle lineStyle = border.LineStyle.Value;
+ bool hasFocus = border.Parent!.HasFocus;
+
+ // Compute tab header geometry
+ Rectangle headerRect = ComputeHeaderRect (borderBounds, border.TabSide, border.TabOffset, tabLength, tabDepth);
+ Rectangle viewBounds = ComputeViewBounds (borderBounds, border.TabSide, tabDepth);
+ Rectangle clipped = Rectangle.Intersect (headerRect, viewBounds);
+ bool tabVisible = !clipped.IsEmpty;
+
+ // Draw the 3 non-tab-side content border lines (always drawn).
+ // The tab-side line is handled below - conditionally, based on whether the tab is visible.
+ if (Adornment!.Thickness.Top > 0 && (border.TabSide != Side.Top || !tabVisible))
+ {
+ lc.AddLine (new Point (borderBounds.X, borderBounds.Y), borderBounds.Width, Orientation.Horizontal, lineStyle, normalAttribute);
+ }
+
+ if (Adornment!.Thickness.Bottom > 0 && (border.TabSide != Side.Bottom || !tabVisible))
+ {
+ lc.AddLine (new Point (borderBounds.X, borderBounds.Bottom - 1), borderBounds.Width, Orientation.Horizontal, lineStyle, normalAttribute);
+ }
+
+ if (Adornment!.Thickness.Left > 0 && (border.TabSide != Side.Left || !tabVisible))
+ {
+ lc.AddLine (new Point (borderBounds.X, borderBounds.Y), borderBounds.Height, Orientation.Vertical, lineStyle, normalAttribute);
+ }
+
+ if (Adornment!.Thickness.Right > 0 && (border.TabSide != Side.Right || !tabVisible))
+ {
+ lc.AddLine (new Point (borderBounds.Right - 1, borderBounds.Y), borderBounds.Height, Orientation.Vertical, lineStyle, normalAttribute);
+ }
+
+ // Draw the tab-side content border (gap segments around the tab)
+ if (tabVisible)
+ {
+ AddTabSideContentBorder (lc,
+ clipped,
+ headerRect,
+ borderBounds,
+ border.TabSide,
+ hasFocus,
+ tabDepth,
+ lineStyle,
+ normalAttribute);
+ }
+
+ // Update LastTitleRect for click handling (uses draw-time geometry)
+ if (tabVisible && _tabTitleView is { Visible: true })
+ {
+ Thickness effectiveThickness = ComputeTabLabelThickness (border.TabSide, tabDepth, hasFocus);
+ Rectangle visibleContent = ComputeTabContentArea (clipped, headerRect, border.TabSide, effectiveThickness, tabDepth);
+ LastTitleRect = visibleContent.IsEmpty ? null : visibleContent;
+ }
+ else
+ {
+ LastTitleRect = null;
+ }
+
+ // Gradient support
+ if (border.Settings.FastHasFlags (BorderSettings.Gradient))
+ {
+ if (_cachedGradientFill is null || _cachedGradientRect != screenBounds)
+ {
+ SetupGradientLineCanvas (lc, screenBounds);
+ }
+ else
+ {
+ lc.Fill = _cachedGradientFill;
+ }
+ }
+ else
+ {
+ lc.Fill = null;
+ _cachedGradientFill = null;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Draws the tab-side content border line. For focused depth ≥ 3, draws split segments
+ /// around the gap. For unfocused depth ≥ 3, draws the full line (auto-join creates junctions).
+ /// For depth < 3, draws the full line.
+ ///
+ private static void AddTabSideContentBorder (LineCanvas lc,
+ Rectangle clipped,
+ Rectangle headerRect,
+ Rectangle contentBorderRect,
+ Side side,
+ bool hasFocus,
+ int depth,
+ LineStyle lineStyle,
+ Attribute? attribute)
+ {
+ // Open gap when: focused at depth >= 3 (no content-side border on tab), or
+ // depth < 3 (content border coincides with tab title row - must not overwrite it).
+ bool openGap = (hasFocus && depth >= 3) || depth < 3;
+
+ switch (side)
+ {
+ case Side.Top:
+ {
+ int borderY = contentBorderRect.Y;
+
+ if (!openGap)
+ {
+ lc.AddLine (new Point (contentBorderRect.X, borderY), contentBorderRect.Width, Orientation.Horizontal, lineStyle, attribute);
+ }
+ else
+ {
+ if (clipped.X > contentBorderRect.X)
+ {
+ lc.AddLine (new Point (contentBorderRect.X, borderY),
+ clipped.X - contentBorderRect.X + 1,
+ Orientation.Horizontal,
+ lineStyle,
+ attribute);
+ }
+
+ if (clipped.Right - 1 < contentBorderRect.Right - 1)
+ {
+ lc.AddLine (new Point (clipped.Right - 1, borderY),
+ contentBorderRect.Right - (clipped.Right - 1),
+ Orientation.Horizontal,
+ lineStyle,
+ attribute);
+ }
+ }
+
+ break;
+ }
+
+ case Side.Bottom:
+ {
+ int borderY = contentBorderRect.Bottom - 1;
+
+ if (!openGap)
+ {
+ lc.AddLine (new Point (contentBorderRect.X, borderY), contentBorderRect.Width, Orientation.Horizontal, lineStyle, attribute);
+ }
+ else
+ {
+ if (clipped.X > contentBorderRect.X)
+ {
+ lc.AddLine (new Point (contentBorderRect.X, borderY),
+ clipped.X - contentBorderRect.X + 1,
+ Orientation.Horizontal,
+ lineStyle,
+ attribute);
+ }
+
+ if (clipped.Right - 1 < contentBorderRect.Right - 1)
+ {
+ lc.AddLine (new Point (clipped.Right - 1, borderY),
+ contentBorderRect.Right - (clipped.Right - 1),
+ Orientation.Horizontal,
+ lineStyle,
+ attribute);
+ }
+ }
+
+ break;
+ }
+
+ case Side.Left:
+ {
+ int borderX = contentBorderRect.X;
+
+ if (!openGap)
+ {
+ lc.AddLine (new Point (borderX, contentBorderRect.Y), contentBorderRect.Height, Orientation.Vertical, lineStyle, attribute);
+ }
+ else
+ {
+ if (clipped.Y > contentBorderRect.Y)
+ {
+ lc.AddLine (new Point (borderX, contentBorderRect.Y), clipped.Y - contentBorderRect.Y + 1, Orientation.Vertical, lineStyle, attribute);
+ }
+ else if (clipped.Y > headerRect.Y)
+ {
+ // Header clipped at top (overflow) - suppress corner glyph
+ lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Y, 1, 1)));
+ }
+
+ if (clipped.Bottom - 1 < contentBorderRect.Bottom - 1)
+ {
+ lc.AddLine (new Point (borderX, clipped.Bottom - 1),
+ contentBorderRect.Bottom - (clipped.Bottom - 1),
+ Orientation.Vertical,
+ lineStyle,
+ attribute);
+ }
+ else if (clipped.Bottom < headerRect.Bottom)
+ {
+ // Header clipped at bottom (overflow) - suppress corner glyph
+ lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Bottom - 1, 1, 1)));
+ }
+ }
+
+ break;
+ }
+
+ case Side.Right:
+ {
+ int borderX = contentBorderRect.Right - 1;
+
+ if (!openGap)
+ {
+ lc.AddLine (new Point (borderX, contentBorderRect.Y), contentBorderRect.Height, Orientation.Vertical, lineStyle, attribute);
+ }
+ else
+ {
+ if (clipped.Y > contentBorderRect.Y)
+ {
+ lc.AddLine (new Point (borderX, contentBorderRect.Y), clipped.Y - contentBorderRect.Y + 1, Orientation.Vertical, lineStyle, attribute);
+ }
+ else if (clipped.Y > headerRect.Y)
+ {
+ // Header clipped at top (overflow) - suppress corner glyph
+ lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Y, 1, 1)));
+ }
+
+ if (clipped.Bottom - 1 < contentBorderRect.Bottom - 1)
+ {
+ lc.AddLine (new Point (borderX, clipped.Bottom - 1),
+ contentBorderRect.Bottom - (clipped.Bottom - 1),
+ Orientation.Vertical,
+ lineStyle,
+ attribute);
+ }
+ else if (clipped.Bottom < headerRect.Bottom)
+ {
+ // Header clipped at bottom (overflow) - suppress corner glyph
+ lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Bottom - 1, 1, 1)));
+ }
+ }
+
+ break;
+ }
+
+ default: throw new ArgumentOutOfRangeException (nameof (side), side, null);
+ }
+ }
+
+ ///
+ /// Computes the content area within the tab header where the title text is drawn.
+ /// For depth ≥ 3, always reserves 1 cell on each side (cap, content-side closing edge,
+ /// side edges) - even when the content-side thickness is 0 (focused gap).
+ /// For depth < 3, uses the actual tab thickness.
+ ///
+ private static Rectangle ComputeTabContentArea (Rectangle clipped, Rectangle headerRect, Side side, Thickness tabThickness, int depth)
+ {
+ // For depth >= 3, always reserve 1 cell on the content side for the closing edge/gap
+ Thickness effectiveThickness = depth >= 3
+ ? side switch
+ {
+ Side.Top => tabThickness with { Bottom = 1 },
+ Side.Bottom => tabThickness with { Top = 1 },
+ Side.Left => tabThickness with { Right = 1 },
+ Side.Right => tabThickness with { Left = 1 },
+ _ => tabThickness
+ }
+ : tabThickness;
+
+ int left = clipped.X + (clipped.X == headerRect.X ? effectiveThickness.Left : 0);
+ int top = clipped.Y + (clipped.Y == headerRect.Y ? effectiveThickness.Top : 0);
+ int right = clipped.Right - (clipped.Right == headerRect.Right ? effectiveThickness.Right : 0);
+ int bottom = clipped.Bottom - (clipped.Bottom == headerRect.Bottom ? effectiveThickness.Bottom : 0);
+
+ int w = right - left;
+ int h = bottom - top;
+
+ if (w <= 0 || h <= 0)
+ {
+ return Rectangle.Empty;
+ }
+
+ return new Rectangle (left, top, w, h);
+ }
+
///
protected override bool OnDrawingContent (DrawContext? context)
{
@@ -219,6 +748,12 @@ protected override bool OnDrawingContent (DrawContext? context)
throw new InvalidOperationException ("Adornment must be of type Border");
}
+ // Tab mode: completely separate codepath with edge-based border positioning
+ if (border.Settings.FastHasFlags (BorderSettings.Tab))
+ {
+ return DrawTabBorder (border);
+ }
+
Rectangle screenBounds = ViewportToScreen (Viewport);
Rectangle borderBounds = GetBorderBounds ();
@@ -453,7 +988,6 @@ protected override bool OnDrawingContent (DrawContext? context)
return true;
}
- ///
///
/// Gets the screen-coordinate rectangle of the title text from the last draw pass.
/// Used by the parent view to build drawn region for transparent border clip exclusion.
diff --git a/Terminal.Gui/ViewBase/Adornment/TabTitleView.cs b/Terminal.Gui/ViewBase/Adornment/TabTitleView.cs
new file mode 100644
index 0000000000..30dd45e674
--- /dev/null
+++ b/Terminal.Gui/ViewBase/Adornment/TabTitleView.cs
@@ -0,0 +1,42 @@
+namespace Terminal.Gui.ViewBase;
+
+///
+/// A lightweight View that renders tab title text using the parent View's focus-appropriate
+/// attributes. Because this View never has focus itself, the base
+/// would always use Normal/HotNormal. This override uses the owning View's
+/// to select Focus/HotFocus when appropriate.
+///
+internal sealed class TabTitleView : View
+{
+ /// The View whose focus state determines which attributes to use.
+ internal View? OwnerView { get; init; }
+
+ /// Sync to (same as Label).
+ public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; }
+
+ ///
+ protected override bool OnDrawingText (DrawContext? context)
+ {
+ if (Driver is null)
+ {
+ return false;
+ }
+
+ bool ownerHasFocus = OwnerView?.HasFocus ?? false;
+
+ Rectangle drawRect = ViewportToScreen ();
+
+ // Add the entire content area to the drawn region so that it is not transparent
+ context?.AddDrawnRectangle (drawRect);
+
+ TextFormatter.Draw (Driver,
+ drawRect,
+ ownerHasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal),
+ ownerHasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal),
+ Rectangle.Empty);
+
+ SetSubViewNeedsDrawDownHierarchy ();
+
+ return true;
+ }
+}
diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs
index d7b4bf3c35..9b70f06849 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,11 +208,26 @@ 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 ();
borderView.DoDrawSubViews ();
+
+ // Merge any LineCanvas lines from Border's SubViews into this View's LineCanvas.
+ // This ensures auto-join works between adornment subview borders and the view's own border.
+ if (borderView.LineCanvas.Bounds != Rectangle.Empty)
+ {
+ LineCanvas.Merge (borderView.LineCanvas);
+ borderView.LineCanvas.Clear ();
+ }
+
SetClip (saved);
// Track drawn subview areas so DoDrawComplete can exclude them from clip
@@ -237,6 +252,16 @@ private void DoDrawAdornmentsSubViews (DrawContext? context)
Region? savedPadding = paddingView.AddFrameToClip ();
paddingView.DoDrawSubViews ();
+
+ // Merge any LineCanvas lines from Padding's SubViews (e.g., TabView's tab headers)
+ // into this View's LineCanvas. This ensures auto-join works between adornment subview
+ // borders and the view's own border.
+ if (paddingView.LineCanvas.Bounds != Rectangle.Empty)
+ {
+ LineCanvas.Merge (paddingView.LineCanvas);
+ paddingView.LineCanvas.Clear ();
+ }
+
SetClip (savedPadding);
// Track drawn subview areas for Padding transparency support.
@@ -734,24 +759,34 @@ public void DrawSubViews (DrawContext? context = null)
return;
}
+ // Track the cumulative drawn region from higher-Z subviews so that when merging
+ // lower-Z subviews' LineCanvas, their lines can be clipped against areas already drawn.
+ Region? priorDrawnRegion = null;
+
// Draw the SubViews in reverse Z-order to leverage clipping.
// SubViews earlier in the collection are drawn last (on top).
foreach (View view in InternalSubViews.Snapshot ().Where (v => v.Visible).Reverse ())
{
- // TODO: HACK - This forcing of SetNeedsDraw with SuperViewRendersLineCanvas enables auto line join to work, but is brute force.
- if (view.SuperViewRendersLineCanvas || view.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent))
- {
- //view.SetNeedsDraw ();
- }
-
view.Draw (context);
if (!view.SuperViewRendersLineCanvas)
{
continue;
}
- LineCanvas.Merge (view.LineCanvas);
+
+ // Merge with clipping: exclude areas already drawn by higher-Z subviews.
+ // This prevents a lower-Z subview's border lines from rendering where a higher-Z
+ // subview already drew (e.g., a focused tab's open gap must not be filled by an
+ // unfocused tab's border). Lines are split at the boundary so auto-join only sees
+ // the higher-Z subview's lines at those cells.
+ LineCanvas.Merge (view.LineCanvas, priorDrawnRegion);
view.LineCanvas.Clear ();
+
+ // Snapshot the drawn region after this subview for the next iteration.
+ if (context is { })
+ {
+ priorDrawnRegion = context.GetDrawnRegion ();
+ }
}
}
@@ -981,12 +1016,6 @@ void cacheAdornmentDrawnRegion (AdornmentImpl adornment, Region? lastLineCanvasR
exclusion.Combine (ViewportToScreen (Viewport), RegionOp.Union);
}
- // Add title rect (drawn directly, not via LineCanvas) to context.
- if (Border.View is BorderView { LastTitleRect: { } titleRect })
- {
- context?.AddDrawnRectangle (titleRect);
- }
-
// For transparent layers, also include context drawn regions (text, content, subviews)
// clipped to the border frame. This ensures transparent view/adornment drawn cells are
// excluded from the clip so they don't get overdrawn by the SuperView.
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/TabCompositionTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/TabCompositionTests.cs
new file mode 100644
index 0000000000..4599e21de8
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/TabCompositionTests.cs
@@ -0,0 +1,643 @@
+using UnitTests;
+
+namespace ViewBaseTests.Adornments;
+
+///
+/// Tests that multiple Views with tab-style borders compose correctly when sharing
+/// a parent's LineCanvas via .
+/// Covers all four sides (Top, Bottom, Left, Right) and tab thicknesses 1, 2, and 3.
+///
+
+// Copilot
+public class TabCompositionTests (ITestOutputHelper output) : TestDriverBase
+{
+ // ─────────────────────────────────────────────────────────────────────
+ // Helpers
+ // ─────────────────────────────────────────────────────────────────────
+
+ private static View CreateTabView (IDriver driver, Side side, int thickness, string title, int tabOffset)
+ {
+ View view = new ()
+ {
+ Driver = driver,
+ CanFocus = true,
+ SuperViewRendersLineCanvas = true,
+ BorderStyle = LineStyle.Rounded,
+ Title = title,
+ Text = $"{title} content",
+ Arrangement = ViewArrangement.Overlapped
+ };
+
+ view.Border.Thickness = side switch
+ {
+ Side.Top => new Thickness (1, thickness, 1, 1),
+ Side.Bottom => new Thickness (1, 1, 1, thickness),
+ Side.Left => new Thickness (thickness, 1, 1, 1),
+ Side.Right => new Thickness (1, 1, thickness, 1),
+ _ => throw new ArgumentOutOfRangeException (nameof (side))
+ };
+
+ view.Border.Settings = BorderSettings.Tab | BorderSettings.Title;
+ view.Border.TabSide = side;
+ view.Border.TabOffset = tabOffset;
+
+ // Fix the dimension perpendicular to the tab edge so that all tab views
+ // share the same extent and composition tests produce stable output.
+ if (side is Side.Top or Side.Bottom)
+ {
+ view.Width = 12;
+ view.Height = Dim.Auto ();
+ }
+ else
+ {
+ view.Width = Dim.Auto ();
+ view.Height = 6;
+ }
+
+ return view;
+ }
+
+ private void DrawAndAssert (View view, IDriver driver, string expected)
+ {
+ view.Layout ();
+ view.Draw ();
+ DriverAssert.AssertDriverContentsAre (expected, output, driver);
+ view.Dispose ();
+ }
+
+ // ═════════════════════════════════════════════════════════════════════
+ // Side.Top — composition (kept from original PR tests)
+ // ═════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void Top_Thickness3_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Top, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Top, 3, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─╮ ╭─╮
+ │A│ │B│
+ │ ╰─┴─┴────╮
+ │A content │
+ ╰──────────╯
+ """);
+ }
+
+ [Fact]
+ public void Top_Thickness3_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Top, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Top, 3, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─╮ ╭─╮
+ │A│ │B│
+ ├─┴─╯ ╰────╮
+ │B content │
+ ╰──────────╯
+ """);
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // Side.Top — thickness 1
+ // ─────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Top_Thickness1_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Top, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Top, 1, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ │A╭─┬─┬────╮
+ │A content │
+ ╰──────────╯
+ """);
+ }
+
+ [Fact]
+ public void Top_Thickness1_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Top, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Top, 1, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─┬─╮B╭────╮
+ │B content │
+ ╰──────────╯
+ """);
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // Side.Top — thickness 2
+ // ─────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Top_Thickness2_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Top, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Top, 2, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─╮ ╭─╮
+ │A╰─┴─┴────╮
+ │A content │
+ ╰──────────╯
+ """);
+ }
+
+ [Fact]
+ public void Top_Thickness2_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Top, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Top, 2, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─╮ ╭─╮
+ ├─┴─╯B╰────╮
+ │B content │
+ ╰──────────╯
+ """);
+ }
+
+ // ═════════════════════════════════════════════════════════════════════
+ // Side.Bottom — thickness 1, 2, 3
+ // ═════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void Bottom_Thickness1_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Bottom, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Bottom, 1, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │A content │
+ │A╭─┬─┬────╯
+ """);
+ }
+
+ [Fact]
+ public void Bottom_Thickness1_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Bottom, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Bottom, 1, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │B content │
+ ├─┬─╮B╭────╯
+ """);
+ }
+
+ [Fact]
+ public void Bottom_Thickness2_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Bottom, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Bottom, 2, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │A content │
+ │A╭─┬─┬────╯
+ ╰─╯ ╰─╯
+ """);
+ }
+
+ [Fact]
+ public void Bottom_Thickness2_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Bottom, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Bottom, 2, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │B content │
+ ├─┬─╮B╭────╯
+ ╰─╯ ╰─╯
+ """);
+ }
+
+ [Fact]
+ public void Bottom_Thickness3_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Bottom, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Bottom, 3, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │A content │
+ │A╭─┬─┬────╯
+ │ │ │B│
+ ╰─╯ ╰─╯
+ """);
+ }
+
+ [Fact]
+ public void Bottom_Thickness3_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Bottom, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Bottom, 3, "B", 4);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │B content │
+ ├─┬─╮B╭────╯
+ │A│ │ │
+ ╰─╯ ╰─╯
+ """);
+ }
+
+ // ═════════════════════════════════════════════════════════════════════
+ // Side.Left — thickness 1, 2, 3
+ // ═════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void Left_Thickness1_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Left, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Left, 1, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ──────────╮
+ AA content│
+ │ │
+ │ │
+ │ │
+ ╰─────────╯
+ """);
+ }
+
+ [Fact]
+ public void Left_Thickness1_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Left, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Left, 1, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─────────╮
+ │B content│
+ │ │
+ │ │
+ B │
+ ──────────╯
+ """);
+ }
+
+ [Fact]
+ public void Left_Thickness2_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Left, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Left, 2, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │AA content│
+ ╰╮ │
+ ╭┤ │
+ ││ │
+ ╰┴─────────╯
+ """);
+ }
+
+ [Fact]
+ public void Left_Thickness2_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Left, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Left, 2, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭┬─────────╮
+ ││B content│
+ ╰┤ │
+ ╭╯ │
+ │B │
+ ╰──────────╯
+ """);
+ }
+
+ [Fact]
+ public void Left_Thickness3_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Left, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Left, 3, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭───────────╮
+ │A A content│
+ ╰─╮ │
+ ╭─┤ │
+ │B│ │
+ ╰─┴─────────╯
+ """);
+ }
+
+ [Fact]
+ public void Left_Thickness3_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Left, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Left, 3, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─┬─────────╮
+ │A│B content│
+ ╰─┤ │
+ ╭─╯ │
+ │B │
+ ╰───────────╯
+ """);
+ }
+
+ // ═════════════════════════════════════════════════════════════════════
+ // Side.Right — thickness 1, 2, 3
+ // ═════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void Right_Thickness1_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Right, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Right, 1, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────
+ │A contentA
+ │ │
+ │ │
+ │ │
+ ╰─────────╯
+ """);
+ }
+
+ [Fact]
+ public void Right_Thickness1_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Right, 1, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Right, 1, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─────────╮
+ │B content│
+ │ │
+ │ │
+ │ B
+ ╰──────────
+ """);
+ }
+
+ [Fact]
+ public void Right_Thickness2_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Right, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Right, 2, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭──────────╮
+ │A contentA│
+ │ ╭╯
+ │ ├╮
+ │ ││
+ ╰─────────┴╯
+ """);
+ }
+
+ [Fact]
+ public void Right_Thickness2_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Right, 2, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Right, 2, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─────────┬╮
+ │B content││
+ │ ├╯
+ │ ╰╮
+ │ B│
+ ╰──────────╯
+ """);
+ }
+
+ [Fact]
+ public void Right_Thickness3_TwoTabs_Tab1Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Right, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Right, 3, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab1.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭───────────╮
+ │A contentA │
+ │ ╭─╯
+ │ ├─╮
+ │ │B│
+ ╰─────────┴─╯
+ """);
+ }
+
+ [Fact]
+ public void Right_Thickness3_TwoTabs_Tab2Focused ()
+ {
+ IDriver driver = CreateTestDriver (25, 12);
+
+ View tab1 = CreateTabView (driver, Side.Right, 3, "A", 0);
+ View tab2 = CreateTabView (driver, Side.Right, 3, "B", 3);
+
+ View superView = new () { CanFocus = true, Driver = driver, Width = Dim.Fill (), Height = Dim.Fill () };
+ superView.Add (tab1, tab2);
+ tab2.SetFocus ();
+
+ DrawAndAssert (superView,
+ driver,
+ """
+ ╭─────────┬─╮
+ │B content│A│
+ │ ├─╯
+ │ ╰─╮
+ │ B │
+ ╰───────────╯
+ """);
+ }
+}