diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 844e178d5b..292e27276b 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -62,6 +62,10 @@ public class CharMap : View, IDesignable, IValue // ReSharper disable once InconsistentNaming private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End); + private static readonly int MAX_ROW = MAX_CODE_POINT / 16; + private const int SURROGATE_ROW_START = 0xD800 / 16; + private const int SURROGATE_ROW_END = 0xDFFF / 16; + private const int SURROGATE_ROW_COUNT = SURROGATE_ROW_END - SURROGATE_ROW_START + 1; /// /// Initializes a new instance. @@ -148,16 +152,29 @@ public CharMap () Cursor = new Cursor { Style = DefaultCursorStyle }; } - // Visible rows management: each entry is the starting code point of a 16-wide row - private readonly List _visibleRowStarts = []; - private readonly Dictionary _rowStartToVisibleIndex = []; + // Visible rows management when filtering by Unicode category. + private List? _visibleRowStarts; + private Dictionary? _rowStartToVisibleIndex; private void RebuildVisibleRows () { + if (!ShowUnicodeCategory.HasValue) + { + _visibleRowStarts = null; + _rowStartToVisibleIndex = null; + SetContentSize (new Size (COLUMN_WIDTH * 16 + RowLabelWidth, VisibleRowCount * _rowHeight + HEADER_HEIGHT)); + VerticalScrollBar.ScrollableContentSize = GetContentHeight (); + + return; + } + + _visibleRowStarts ??= []; + _rowStartToVisibleIndex ??= []; + _visibleRowStarts.Clear (); _rowStartToVisibleIndex.Clear (); - int maxRow = MAX_CODE_POINT / 16; + int maxRow = MAX_ROW; for (var row = 0; row <= maxRow; row++) { @@ -209,17 +226,107 @@ private void RebuildVisibleRows () } // Update content size to match visible rows - SetContentSize (new Size (COLUMN_WIDTH * 16 + RowLabelWidth, _visibleRowStarts.Count * _rowHeight + HEADER_HEIGHT)); + SetContentSize (new Size (COLUMN_WIDTH * 16 + RowLabelWidth, VisibleRowCount * _rowHeight + HEADER_HEIGHT)); // Keep vertical scrollbar aligned with new content size VerticalScrollBar.ScrollableContentSize = GetContentHeight (); } + private int VisibleRowCount + { + get + { + if (ShowUnicodeCategory.HasValue) + { + return _visibleRowStarts?.Count ?? 0; + } + + int rowCount = MAX_ROW + 1; + + if (MAX_ROW >= SURROGATE_ROW_START) + { + rowCount -= Math.Min (MAX_ROW, SURROGATE_ROW_END) - SURROGATE_ROW_START + 1; + } + + return rowCount; + } + } + private int VisibleRowIndexForCodePoint (int codePoint) { int start = codePoint / 16 * 16; - return _rowStartToVisibleIndex.GetValueOrDefault (start, -1); + if (ShowUnicodeCategory.HasValue) + { + return _rowStartToVisibleIndex?.GetValueOrDefault (start, -1) ?? -1; + } + + int row = start / 16; + + if (row > MAX_ROW || row is >= SURROGATE_ROW_START and <= SURROGATE_ROW_END) + { + return -1; + } + + return row > SURROGATE_ROW_END ? row - SURROGATE_ROW_COUNT : row; + } + + private bool TryGetVisibleRowStart (int visibleRow, out int rowStart) + { + if (visibleRow < 0 || visibleRow >= VisibleRowCount) + { + rowStart = 0; + + return false; + } + + if (ShowUnicodeCategory.HasValue) + { + rowStart = _visibleRowStarts! [visibleRow]; + + return true; + } + + int row = visibleRow >= SURROGATE_ROW_START ? visibleRow + SURROGATE_ROW_COUNT : visibleRow; + rowStart = row * 16; + + return rowStart <= MAX_CODE_POINT; + } + + private bool TryGetNearestVisibleRowStart (int desiredRowStart, out int rowStart) + { + if (ShowUnicodeCategory.HasValue) + { + List? visibleRowStarts = _visibleRowStarts; + int idx = visibleRowStarts?.FindIndex (s => s >= desiredRowStart) ?? -1; + + if (idx < 0 && visibleRowStarts?.Count > 0) + { + idx = visibleRowStarts.Count - 1; + } + + if (idx >= 0) + { + rowStart = visibleRowStarts! [idx]; + + return true; + } + + rowStart = 0; + + return false; + } + + int row = Math.Clamp (desiredRowStart / 16, 0, MAX_ROW); + + if (row is >= SURROGATE_ROW_START and <= SURROGATE_ROW_END) + { + row = SURROGATE_ROW_END + 1; + } + + rowStart = Math.Min (row * 16, MAX_CODE_POINT); + + return true; } private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested @@ -357,19 +464,11 @@ public UnicodeCategory? ShowUnicodeCategory // Ensure selection is on a visible row int desiredRowStart = SelectedCodePoint / 16 * 16; - if (!_rowStartToVisibleIndex.ContainsKey (desiredRowStart)) + if (VisibleRowIndexForCodePoint (desiredRowStart) < 0) { - // Find nearest visible row (prefer next; fallback to last) - int idx = _visibleRowStarts.FindIndex (s => s >= desiredRowStart); - - if (idx < 0 && _visibleRowStarts.Count > 0) + if (TryGetNearestVisibleRowStart (desiredRowStart, out int rowStart)) { - idx = _visibleRowStarts.Count - 1; - } - - if (idx >= 0) - { - SelectedCodePoint = _visibleRowStarts [idx]; + SelectedCodePoint = rowStart; } } @@ -692,7 +791,7 @@ protected override bool OnDrawingContent (DrawContext? context) // Which visible row is this? int visibleRow = (y + Viewport.Y - 1) / _rowHeight; - if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count) + if (!TryGetVisibleRowStart (visibleRow, out int rowStart)) { // No row at this y; clear label area and continue Move (0, y); @@ -701,8 +800,6 @@ protected override bool OnDrawingContent (DrawContext? context) continue; } - int rowStart = _visibleRowStarts [visibleRow]; - // Draw the row label (U+XXXX_) SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active); Move (0, y); @@ -1042,7 +1139,7 @@ private bool TryGetCodePointFromPosition (Point position, out int codePoint) int visibleRow = (position.Y - 1 - -Viewport.Y) / _rowHeight; - if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count) + if (!TryGetVisibleRowStart (visibleRow, out int rowStart)) { codePoint = 0; @@ -1056,7 +1153,7 @@ private bool TryGetCodePointFromPosition (Point position, out int codePoint) col = 15; } - codePoint = _visibleRowStarts [visibleRow] + col; + codePoint = rowStart + col; if (codePoint > MAX_CODE_POINT) { diff --git a/Tests/UnitTestsParallelizable/Views/CharMapTests.cs b/Tests/UnitTestsParallelizable/Views/CharMapTests.cs index 223f176186..03f50ee264 100644 --- a/Tests/UnitTestsParallelizable/Views/CharMapTests.cs +++ b/Tests/UnitTestsParallelizable/Views/CharMapTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text; using UnitTests; @@ -9,6 +10,53 @@ namespace ViewsTests; /// public class CharMapTests : TestDriverBase { + // Copilot + [Fact] + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void Constructor_Default_Does_Not_Preallocate_Visible_Row_Index () + { + using (new CharMap ()) + { } + + GC.Collect (); + GC.WaitForPendingFinalizers (); + GC.Collect (); + + long before = GC.GetAllocatedBytesForCurrentThread (); + using CharMap charMap = new (); + long allocated = GC.GetAllocatedBytesForCurrentThread () - before; + + Assert.True (allocated < 1_000_000, $"Expected CharMap construction to allocate less than 1 MB, but allocated {allocated:N0} bytes."); + + int maxRow = UnicodeRange.Ranges.Max (r => r.End) / 16; + int surrogateRowStart = 0xD800 / 16; + int surrogateRowEnd = 0xDFFF / 16; + int surrogateRows = Math.Min (maxRow, surrogateRowEnd) - surrogateRowStart + 1; + int expectedContentHeight = maxRow + 1 - surrogateRows + 1; + + Assert.Equal (expectedContentHeight, charMap.GetContentHeight ()); + } + + // Copilot + [Fact] + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void ShowUnicodeCategory_Rebuilds_Filtered_Index_And_Can_Clear_Filter () + { + using CharMap charMap = new () { SelectedCodePoint = 'a' }; + int defaultContentHeight = charMap.GetContentHeight (); + + charMap.ShowUnicodeCategory = UnicodeCategory.UppercaseLetter; + + Assert.True (charMap.GetContentHeight () < defaultContentHeight); + Assert.Equal (UnicodeCategory.UppercaseLetter, CharUnicodeInfo.GetUnicodeCategory (charMap.SelectedCodePoint)); + + charMap.ShowUnicodeCategory = null; + + Assert.Equal (defaultContentHeight, charMap.GetContentHeight ()); + } + /// /// Verifies that is raised when changes. ///