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); } }