diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
index 4a69a96b7..8018e16b5 100644
--- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
+++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
@@ -1,4 +1,7 @@
#nullable enable
+using System.Buffers;
+using System.Runtime.InteropServices;
+
namespace Terminal.Gui;
/// Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines.
@@ -161,18 +164,25 @@ public class LineCanvas : IDisposable
{
Dictionary map = new ();
+ List intersectionsBufferList = [];
+
// walk through each pixel of the bitmap
for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++)
{
for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++)
{
- IntersectionDefinition? [] intersects = _lines
- .Select (l => l.Intersects (x, y))
- .Where (i => i is { })
- .ToArray ();
-
+ intersectionsBufferList.Clear ();
+ foreach (var line in _lines)
+ {
+ if (line.Intersects (x, y) is IntersectionDefinition intersect)
+ {
+ intersectionsBufferList.Add (intersect);
+ }
+ }
+ // Safe as long as the list is not modified while the span is in use.
+ ReadOnlySpan intersects = CollectionsMarshal.AsSpan(intersectionsBufferList);
Cell? cell = GetCellForIntersects (Application.Driver, intersects);
-
+ // TODO: Can we skip the whole nested looping if _exclusionRegion is null?
if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false)
{
map.Add (new (x, y), cell);
@@ -207,10 +217,11 @@ public class LineCanvas : IDisposable
{
for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
{
- IntersectionDefinition? [] intersects = _lines
- .Select (l => l.Intersects (x, y))
- .Where (i => i is { })
- .ToArray ();
+ IntersectionDefinition [] intersects = _lines
+ // ! nulls are filtered out by the next Where filter
+ .Select (l => l.Intersects (x, y)!)
+ .Where (i => i is not null)
+ .ToArray ();
Rune? rune = GetRuneForIntersects (Application.Driver, intersects);
@@ -315,9 +326,16 @@ public class LineCanvas : IDisposable
return sb.ToString ();
}
- private static bool All (IntersectionDefinition? [] intersects, Orientation orientation)
+ private static bool All (ReadOnlySpan intersects, Orientation orientation)
{
- return intersects.All (i => i!.Line.Orientation == orientation);
+ foreach (var intersect in intersects)
+ {
+ if (intersect.Line.Orientation != orientation)
+ {
+ return false;
+ }
+ }
+ return true;
}
private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
@@ -337,9 +355,9 @@ public class LineCanvas : IDisposable
///
private static bool Exactly (HashSet intersects, params IntersectionType [] types) { return intersects.SetEquals (types); }
- private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects)
+ private Attribute? GetAttributeForIntersects (ReadOnlySpan intersects)
{
- return Fill?.GetAttribute (intersects [0]!.Point) ?? intersects [0]!.Line.Attribute;
+ return Fill?.GetAttribute (intersects [0].Point) ?? intersects [0].Line.Attribute;
}
private readonly Dictionary _runeResolvers = new ()
@@ -384,9 +402,9 @@ public class LineCanvas : IDisposable
// TODO: Add other resolvers
};
- private Cell? GetCellForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
+ private Cell? GetCellForIntersects (IConsoleDriver? driver, ReadOnlySpan intersects)
{
- if (!intersects.Any ())
+ if (intersects.IsEmpty)
{
return null;
}
@@ -404,37 +422,28 @@ public class LineCanvas : IDisposable
return cell;
}
- private Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
+ private Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan intersects)
{
- if (!intersects.Any ())
+ if (intersects.IsEmpty)
{
return null;
}
IntersectionRuneType runeType = GetRuneTypeForIntersects (intersects);
-
if (_runeResolvers.TryGetValue (runeType, out IntersectionRuneResolver? resolver))
{
return resolver.GetRuneForIntersects (driver, intersects);
}
// TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
- bool useDouble = intersects.Any (i => i?.Line.Style == LineStyle.Double);
-
- bool useDashed = intersects.Any (
- i => i?.Line.Style == LineStyle.Dashed
- || i?.Line.Style == LineStyle.RoundedDashed
- );
-
- bool useDotted = intersects.Any (
- i => i?.Line.Style == LineStyle.Dotted
- || i?.Line.Style == LineStyle.RoundedDotted
- );
+ bool useDouble = AnyLineStyles(intersects, [LineStyle.Double]);
+ bool useDashed = AnyLineStyles(intersects, [LineStyle.Dashed, LineStyle.RoundedDashed]);
+ bool useDotted = AnyLineStyles(intersects, [LineStyle.Dotted, LineStyle.RoundedDotted]);
// horiz and vert lines same as Single for Rounded
- bool useThick = intersects.Any (i => i?.Line.Style == LineStyle.Heavy);
- bool useThickDashed = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDashed);
- bool useThickDotted = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDotted);
+ bool useThick = AnyLineStyles(intersects, [LineStyle.Heavy]);
+ bool useThickDashed = AnyLineStyles(intersects, [LineStyle.HeavyDashed]);
+ bool useThickDotted = AnyLineStyles(intersects, [LineStyle.HeavyDotted]);
// TODO: Support ruler
//var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
@@ -493,11 +502,31 @@ public class LineCanvas : IDisposable
+ runeType
);
}
+
+
+ static bool AnyLineStyles (ReadOnlySpan intersects, ReadOnlySpan lineStyles)
+ {
+ foreach (IntersectionDefinition intersect in intersects)
+ {
+ foreach (LineStyle style in lineStyles)
+ {
+ if (intersect.Line.Style == style)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
- private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition? [] intersects)
+ private IntersectionRuneType GetRuneTypeForIntersects (ReadOnlySpan intersects)
{
- HashSet set = new (intersects.Select (i => i!.Type));
+ HashSet set = new (capacity: intersects.Length);
+ foreach (var intersect in intersects)
+ {
+ set.Add (intersect.Type);
+ }
#region Cross Conditions
@@ -683,7 +712,17 @@ public class LineCanvas : IDisposable
///
///
///
- private bool Has (HashSet intersects, params IntersectionType [] types) { return types.All (t => intersects.Contains (t)); }
+ private bool Has (HashSet intersects, params IntersectionType [] types)
+ {
+ foreach (var type in types)
+ {
+ if (!intersects.Contains (type))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver
{
@@ -727,45 +766,12 @@ public class LineCanvas : IDisposable
internal Rune _thickV;
protected IntersectionRuneResolver () { SetGlyphs (); }
- public Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects)
+ public Rune? GetRuneForIntersects (IConsoleDriver? driver, ReadOnlySpan intersects)
{
- bool useRounded = intersects.Any (
- i => i?.Line.Length != 0
- && (
- i?.Line.Style == LineStyle.Rounded
- || i?.Line.Style
- == LineStyle.RoundedDashed
- || i?.Line.Style
- == LineStyle.RoundedDotted)
- );
-
// Note that there aren't any glyphs for intersections of double lines with heavy lines
- bool doubleHorizontal = intersects.Any (
- l => l?.Line.Orientation == Orientation.Horizontal
- && l.Line.Style == LineStyle.Double
- );
-
- bool doubleVertical = intersects.Any (
- l => l?.Line.Orientation == Orientation.Vertical
- && l.Line.Style == LineStyle.Double
- );
-
- bool thickHorizontal = intersects.Any (
- l => l?.Line.Orientation == Orientation.Horizontal
- && (
- l.Line.Style == LineStyle.Heavy
- || l.Line.Style == LineStyle.HeavyDashed
- || l.Line.Style == LineStyle.HeavyDotted)
- );
-
- bool thickVertical = intersects.Any (
- l => l?.Line.Orientation == Orientation.Vertical
- && (
- l.Line.Style == LineStyle.Heavy
- || l.Line.Style == LineStyle.HeavyDashed
- || l.Line.Style == LineStyle.HeavyDotted)
- );
+ bool doubleHorizontal = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Horizontal, [LineStyle.Double]);
+ bool doubleVertical = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Vertical, [LineStyle.Double]);
if (doubleHorizontal)
{
@@ -777,6 +783,11 @@ public class LineCanvas : IDisposable
return _doubleV;
}
+ bool thickHorizontal = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Horizontal,
+ [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]);
+ bool thickVertical = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Vertical,
+ [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]);
+
if (thickHorizontal)
{
return thickVertical ? _thickBoth : _thickH;
@@ -787,7 +798,51 @@ public class LineCanvas : IDisposable
return _thickV;
}
- return useRounded ? _round : _normal;
+ return UseRounded (intersects) ? _round : _normal;
+
+ static bool UseRounded (ReadOnlySpan intersects)
+ {
+ foreach (var intersect in intersects)
+ {
+ if (intersect.Line.Length == 0)
+ {
+ continue;
+ }
+
+ if (intersect.Line.Style is
+ LineStyle.Rounded or
+ LineStyle.RoundedDashed or
+ LineStyle.RoundedDotted)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static bool AnyWithOrientationAndAnyLineStyle (
+ ReadOnlySpan intersects,
+ Orientation orientation,
+ ReadOnlySpan lineStyles)
+ {
+ foreach (var i in intersects)
+ {
+ if (i.Line.Orientation != orientation)
+ {
+ continue;
+ }
+
+ // Any line style
+ foreach (var style in lineStyles)
+ {
+ if (i.Line.Style == style)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
///
diff --git a/Terminal.Gui/Drawing/Region.cs b/Terminal.Gui/Drawing/Region.cs
index c3ef1d61b..9929a1796 100644
--- a/Terminal.Gui/Drawing/Region.cs
+++ b/Terminal.Gui/Drawing/Region.cs
@@ -1,4 +1,8 @@
-///
+#nullable enable
+
+using System.Buffers;
+
+///
/// Represents a region composed of one or more rectangles, providing methods for union, intersection, exclusion, and
/// complement operations.
///
@@ -43,7 +47,41 @@ public class Region : IDisposable
/// The rectangle to intersect with the region.
public void Intersect (Rectangle rectangle)
{
- _rectangles = _rectangles.Select (r => Rectangle.Intersect (r, rectangle)).Where (r => !r.IsEmpty).ToList ();
+ if (_rectangles.Count == 0)
+ {
+ return;
+ }
+ // TODO: In-place swap within the original list. Does order of intersections matter?
+ // Rectangle = 4 * i32 = 16 B
+ // ~128 B stack allocation
+ const int maxStackallocLength = 8;
+ Rectangle []? rentedArray = null;
+ try
+ {
+ Span rectBuffer = _rectangles.Count <= maxStackallocLength
+ ? stackalloc Rectangle[maxStackallocLength]
+ : (rentedArray = ArrayPool.Shared.Rent (_rectangles.Count));
+
+ _rectangles.CopyTo (rectBuffer);
+ ReadOnlySpan rectangles = rectBuffer[.._rectangles.Count];
+ _rectangles.Clear ();
+
+ foreach (var rect in rectangles)
+ {
+ Rectangle intersection = Rectangle.Intersect (rect, rectangle);
+ if (!intersection.IsEmpty)
+ {
+ _rectangles.Add (intersection);
+ }
+ }
+ }
+ finally
+ {
+ if (rentedArray != null)
+ {
+ ArrayPool.Shared.Return (rentedArray);
+ }
+ }
}
///
@@ -154,14 +192,34 @@ public class Region : IDisposable
/// The x-coordinate of the point.
/// The y-coordinate of the point.
/// true if the point is contained within the region; otherwise, false.
- public bool Contains (int x, int y) { return _rectangles.Any (r => r.Contains (x, y)); }
+ public bool Contains (int x, int y)
+ {
+ foreach (var rect in _rectangles)
+ {
+ if (rect.Contains (x, y))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
///
/// Determines whether the specified rectangle is contained within the region.
///
/// The rectangle to check for containment.
/// true if the rectangle is contained within the region; otherwise, false.
- public bool Contains (Rectangle rectangle) { return _rectangles.Any (r => r.Contains (rectangle)); }
+ public bool Contains (Rectangle rectangle)
+ {
+ foreach (var rect in _rectangles)
+ {
+ if (rect.Contains (rectangle))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
///
/// Returns an array of rectangles that represent the region.
diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs
index f426d7659..d4c18bedc 100644
--- a/Terminal.Gui/Text/TextFormatter.cs
+++ b/Terminal.Gui/Text/TextFormatter.cs
@@ -1926,12 +1926,12 @@ public class TextFormatter
private static int GetRuneWidth (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
{
- return GetRuneWidth (str.EnumerateRunes ().ToList (), tabWidth, textDirection);
- }
-
- private static int GetRuneWidth (List runes, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
- {
- return runes.Sum (r => GetRuneWidth (r, tabWidth, textDirection));
+ int runesWidth = 0;
+ foreach (Rune rune in str.EnumerateRunes ())
+ {
+ runesWidth += GetRuneWidth (rune, tabWidth, textDirection);
+ }
+ return runesWidth;
}
private static int GetRuneWidth (Rune rune, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
diff --git a/Terminal.Gui/View/Layout/PosAlign.cs b/Terminal.Gui/View/Layout/PosAlign.cs
index d488d0bd8..1500e5a24 100644
--- a/Terminal.Gui/View/Layout/PosAlign.cs
+++ b/Terminal.Gui/View/Layout/PosAlign.cs
@@ -1,7 +1,6 @@
#nullable enable
using System.ComponentModel;
-using System.Text.RegularExpressions;
namespace Terminal.Gui;
@@ -61,33 +60,27 @@ public record PosAlign : Pos
///
public static int CalculateMinDimension (int groupId, IList views, Dimension dimension)
{
- List dimensionsList = new ();
-
- // PERF: If this proves a perf issue, consider caching a ref to this list in each item
- List viewsInGroup = views.Where (v => HasGroupId (v, dimension, groupId)).ToList ();
-
- if (viewsInGroup.Count == 0)
+ int dimensionsSum = 0;
+ foreach (var view in views)
{
- return 0;
- }
+ if (!HasGroupId (view, dimension, groupId)) {
+ continue;
+ }
- // PERF: We iterate over viewsInGroup multiple times here.
-
- // Update the dimensionList with the sizes of the views
- for (var index = 0; index < viewsInGroup.Count; index++)
- {
- View view = viewsInGroup [index];
-
- PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
+ PosAlign? posAlign = dimension == Dimension.Width
+ ? view.X as PosAlign
+ : view.Y as PosAlign;
if (posAlign is { })
{
- dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
+ dimensionsSum += dimension == Dimension.Width
+ ? view.Frame.Width
+ : view.Frame.Height;
}
}
// Align
- return dimensionsList.Sum ();
+ return dimensionsSum;
}
internal static bool HasGroupId (View v, Dimension dimension, int groupId)