Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 117 additions & 3 deletions Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Merges one line canvas into this one, excluding lines (or portions of lines) that fall
/// within <paramref name="exclude"/>. 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.
/// </summary>
/// <param name="lineCanvas">The source canvas to merge from.</param>
/// <param name="exclude">
/// The region to exclude. Cells of incoming lines that fall within this region will not be merged.
/// Pass <see langword="null"/> for default merge behavior (no exclusion).
/// </param>
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));
}
}

Expand Down
87 changes: 83 additions & 4 deletions Terminal.Gui/ViewBase/Adornment/Border.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ protected override AdornmentView CreateView ()
return bv;
}

/// <inheritdoc />
/// <inheritdoc/>
public override Rectangle GetFrame () => Parent is { } ? Parent.Margin.Thickness.GetInside (Parent!.Margin.GetFrame ()) : Rectangle.Empty;

/// <inheritdoc />
/// <inheritdoc/>
protected override void OnThicknessChanged ()
{
base.OnThicknessChanged ();
Expand All @@ -48,7 +48,8 @@ protected override void OnThicknessChanged ()

/// <summary>
/// Sets the style of the lines drawn in the <see cref="Border"/>. If not set, will inherit the style from
/// the <see cref="IAdornment.Parent"/>'s <see cref="View.SuperView"/>'s <see cref="View.BorderStyle"/>. If set, will cause <see cref="IAdornment.View"/>
/// the <see cref="IAdornment.Parent"/>'s <see cref="View.SuperView"/>'s <see cref="View.BorderStyle"/>. If set, will
/// cause <see cref="IAdornment.View"/>
/// to be created.
/// </summary>
public LineStyle? LineStyle
Expand All @@ -63,7 +64,7 @@ public LineStyle? LineStyle

field = value;

if (field is not null)
if (field is { })
{
GetOrCreateView ();
}
Expand All @@ -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;

/// <summary>Fired when <see cref="Settings"/> changes.</summary>
public event EventHandler? SettingsChanged;

/// <summary>
/// Gets or sets which side the Tab protrudes from. Only used when <see cref="BorderSettings.Tab"/> is set.
/// </summary>
public Side TabSide
{
get;
set
{
if (field == value)
{
return;
}

field = value;
Parent?.SetNeedsLayout ();
}
} = Side.Top;

/// <summary>
/// Gets or sets the offset along the border edge where the Tab starts (columns for Top/Bottom,
/// rows for Left/Right). Only used when <see cref="BorderSettings.Tab"/> is set.
/// </summary>
public int TabOffset
{
get;
set
{
if (field == value)
{
return;
}

field = value;
Parent?.SetNeedsLayout ();
}
}

/// <summary>
/// Gets or sets the total length of the tab parallel to the border edge (including border cells).
/// If <see langword="null"/> the length will be determined based on <see cref="View.Title"/>.
/// Only used when <see cref="BorderSettings.Tab"/> is set.
/// </summary>
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 ();
}
}
}
8 changes: 8 additions & 0 deletions Terminal.Gui/ViewBase/Adornment/BorderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ public enum BorderSettings
/// Use <see cref="GradientFill"/> to draw the border.
/// </summary>
Gradient = 2,

/// <summary>
/// Draw a Tab on one side of the border. The <see cref="View.Title"/> will be displayed in the Tab. Configure with
/// <see cref="Border.TabSide"/>, <see cref="Border.TabOffset"/>,
/// <see cref="Border.TabLength"/>.
/// </summary>
Tab = 4,
}

Loading