diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs
index f7da577ad..6e9aff593 100644
--- a/Terminal.Gui/Drawing/Cell.cs
+++ b/Terminal.Gui/Drawing/Cell.cs
@@ -27,7 +27,7 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, st
readonly get => _grapheme;
set
{
- if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1)
+ if (GraphemeHelper.GetGraphemeCount (value) > 1)
{
throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell.");
}
diff --git a/Terminal.Gui/Drawing/GraphemeHelper.cs b/Terminal.Gui/Drawing/GraphemeHelper.cs
index 4ae00148c..918270727 100644
--- a/Terminal.Gui/Drawing/GraphemeHelper.cs
+++ b/Terminal.Gui/Drawing/GraphemeHelper.cs
@@ -46,4 +46,27 @@ public static class GraphemeHelper
yield return element;
}
}
+
+ ///
+ /// Counts the number of grapheme clusters in a string without allocating intermediate collections.
+ ///
+ /// The string to count graphemes in.
+ /// The number of grapheme clusters, or 0 if the string is null or empty.
+ public static int GetGraphemeCount (string text)
+ {
+ if (string.IsNullOrEmpty (text))
+ {
+ return 0;
+ }
+
+ TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (text);
+ var count = 0;
+
+ while (enumerator.MoveNext ())
+ {
+ count++;
+ }
+
+ return count;
+ }
}
diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
index 763467096..2be985764 100644
--- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
+++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
@@ -211,15 +211,23 @@ public class LineCanvas : IDisposable
{
Dictionary map = new ();
+ List intersectionsBufferList = [];
+
// walk through each pixel of the bitmap
for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++)
{
for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
{
- IntersectionDefinition [] intersects = _lines
- .Select (l => l.Intersects (x, y))
- .OfType () // automatically filters nulls and casts
- .ToArray ();
+ intersectionsBufferList.Clear ();
+ foreach (var line in _lines)
+ {
+ if (line.Intersects (x, y) is { } intersect)
+ {
+ intersectionsBufferList.Add (intersect);
+ }
+ }
+ // Safe as long as the list is not modified while the span is in use.
+ ReadOnlySpan intersects = CollectionsMarshal.AsSpan(intersectionsBufferList);
Rune? rune = GetRuneForIntersects (intersects);
diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs
index f81f4c2b3..c23260d28 100644
--- a/Terminal.Gui/Text/TextFormatter.cs
+++ b/Terminal.Gui/Text/TextFormatter.cs
@@ -123,11 +123,31 @@ public class TextFormatter
}
string strings = linesFormatted [line];
- string[] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
+
+ // Use ArrayPool to avoid per-draw allocations
+ int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters
+ string [] graphemes = ArrayPool.Shared.Rent (estimatedCount);
+ var graphemeCount = 0;
- // When text is justified, we lost left or right, so we use the direction to align.
+ try
+ {
+ foreach (string grapheme in GraphemeHelper.GetGraphemes (strings))
+ {
+ if (graphemeCount >= graphemes.Length)
+ {
+ // Need larger array (rare case for complex text)
+ string [] larger = ArrayPool.Shared.Rent (graphemes.Length * 2);
+ Array.Copy (graphemes, larger, graphemeCount);
+ ArrayPool.Shared.Return (graphemes, clearArray: true);
+ graphemes = larger;
+ }
- int x = 0, y = 0;
+ graphemes [graphemeCount++] = grapheme;
+ }
+
+ // When text is justified, we lost left or right, so we use the direction to align.
+
+ int x = 0, y = 0;
// Horizontal Alignment
if (Alignment is Alignment.End)
@@ -214,7 +234,7 @@ public class TextFormatter
{
if (isVertical)
{
- y = screen.Bottom - graphemes.Length;
+ y = screen.Bottom - graphemeCount;
}
else
{
@@ -250,7 +270,7 @@ public class TextFormatter
{
if (isVertical)
{
- int s = (screen.Height - graphemes.Length) / 2;
+ int s = (screen.Height - graphemeCount) / 2;
y = screen.Top + s;
}
else
@@ -292,17 +312,17 @@ public class TextFormatter
continue;
}
- if (!FillRemaining && idx > graphemes.Length - 1)
+ if (!FillRemaining && idx > graphemeCount - 1)
{
break;
}
if ((!isVertical
&& (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
- || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))
+ || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))
|| (isVertical
&& ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
- || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))))
+ || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))))
{
break;
}
@@ -317,7 +337,7 @@ public class TextFormatter
if (isVertical)
{
- if (idx >= 0 && idx < graphemes.Length)
+ if (idx >= 0 && idx < graphemeCount)
{
text = graphemes [idx];
}
@@ -368,7 +388,7 @@ public class TextFormatter
{
driver?.Move (current, y);
- if (idx >= 0 && idx < graphemes.Length)
+ if (idx >= 0 && idx < graphemeCount)
{
text = graphemes [idx];
}
@@ -428,15 +448,20 @@ public class TextFormatter
current += runeWidth;
}
- int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemes.Length
+ int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemeCount
? graphemes [idx + 1].GetColumns ()
: 0;
- if (!isVertical && idx + 1 < graphemes.Length && current + nextRuneWidth > start + size)
+ if (!isVertical && idx + 1 < graphemeCount && current + nextRuneWidth > start + size)
{
break;
}
}
+ }
+ finally
+ {
+ ArrayPool.Shared.Return (graphemes, clearArray: true);
+ }
}
}
@@ -931,10 +956,30 @@ public class TextFormatter
}
string strings = linesFormatted [line];
- string [] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
+
+ // Use ArrayPool to avoid per-line allocations
+ int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters
+ string [] graphemes = ArrayPool.Shared.Rent (estimatedCount);
+ var graphemeCount = 0;
- // When text is justified, we lost left or right, so we use the direction to align.
- int x = 0, y = 0;
+ try
+ {
+ foreach (string grapheme in GraphemeHelper.GetGraphemes (strings))
+ {
+ if (graphemeCount >= graphemes.Length)
+ {
+ // Need larger array (rare case for complex text)
+ string [] larger = ArrayPool.Shared.Rent (graphemes.Length * 2);
+ Array.Copy (graphemes, larger, graphemeCount);
+ ArrayPool.Shared.Return (graphemes, clearArray: true);
+ graphemes = larger;
+ }
+
+ graphemes [graphemeCount++] = grapheme;
+ }
+
+ // When text is justified, we lost left or right, so we use the direction to align.
+ int x = 0, y = 0;
switch (Alignment)
{
@@ -1011,7 +1056,7 @@ public class TextFormatter
{
// Vertical Alignment
case Alignment.End when isVertical:
- y = screen.Bottom - graphemes.Length;
+ y = screen.Bottom - graphemeCount;
break;
case Alignment.End:
@@ -1041,7 +1086,7 @@ public class TextFormatter
}
case Alignment.Center when isVertical:
{
- int s = (screen.Height - graphemes.Length) / 2;
+ int s = (screen.Height - graphemeCount) / 2;
y = screen.Top + s;
break;
@@ -1081,22 +1126,22 @@ public class TextFormatter
continue;
}
- if (!FillRemaining && idx > graphemes.Length - 1)
+ if (!FillRemaining && idx > graphemeCount - 1)
{
break;
}
if ((!isVertical
&& (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
- || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))
+ || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))
|| (isVertical
&& ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
- || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))))
+ || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))))
{
break;
}
- string text = idx >= 0 && idx < graphemes.Length ? graphemes [idx] : " ";
+ string text = idx >= 0 && idx < graphemeCount ? graphemes [idx] : " ";
int runeWidth = GetStringWidth (text, TabWidth);
if (isVertical)
@@ -1116,20 +1161,25 @@ public class TextFormatter
current += isVertical && runeWidth > 0 ? 1 : runeWidth;
- int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemes.Length
+ int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemeCount
? graphemes [idx + 1].GetColumns ()
: 0;
- if (!isVertical && idx + 1 < graphemes.Length && current + nextStringWidth > start + size)
+ if (!isVertical && idx + 1 < graphemeCount && current + nextStringWidth > start + size)
{
break;
}
}
- // Add the line's drawn region to the overall region
- if (lineWidth > 0 && lineHeight > 0)
+ // Add the line's drawn region to the overall region
+ if (lineWidth > 0 && lineHeight > 0)
+ {
+ drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+ }
+ }
+ finally
{
- drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+ ArrayPool.Shared.Return (graphemes, clearArray: true);
}
}