Skip to content
Merged
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
141 changes: 119 additions & 22 deletions Terminal.Gui/Views/CharMap/CharMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public class CharMap : View, IDesignable, IValue<Rune>

// 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;

/// <summary>
/// Initializes a new instance.
Expand Down Expand Up @@ -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<int> _visibleRowStarts = [];
private readonly Dictionary<int, int> _rowStartToVisibleIndex = [];
// Visible rows management when filtering by Unicode category.
private List<int>? _visibleRowStarts;
private Dictionary<int, int>? _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++)
{
Expand Down Expand Up @@ -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<int>? 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
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;

Expand All @@ -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)
{
Expand Down
48 changes: 48 additions & 0 deletions Tests/UnitTestsParallelizable/Views/CharMapTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using UnitTests;

Expand All @@ -9,6 +10,53 @@ namespace ViewsTests;
/// </summary>
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 ());
}

/// <summary>
/// Verifies that <see cref="CharMap.ValueChangedUntyped"/> is raised when <see cref="CharMap.SelectedCodePoint"/> changes.
/// </summary>
Expand Down
Loading