|
|
@@ -1,4 +1,5 @@
|
|
|
#nullable enable
|
|
|
+using System.Diagnostics;
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
using System.Globalization;
|
|
|
using System.Text.Json;
|
|
|
@@ -15,7 +16,9 @@ public class CharMap : View, IDesignable
|
|
|
{
|
|
|
private const int COLUMN_WIDTH = 3; // Width of each column of glyphs
|
|
|
private const int HEADER_HEIGHT = 1; // Height of the header
|
|
|
- private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested
|
|
|
+
|
|
|
+ // ReSharper disable once InconsistentNaming
|
|
|
+ private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance.
|
|
|
@@ -64,7 +67,8 @@ public class CharMap : View, IDesignable
|
|
|
MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft);
|
|
|
MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight);
|
|
|
|
|
|
- SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + HEADER_HEIGHT));
|
|
|
+ // Initial content size; height will be corrected by RebuildVisibleRows()
|
|
|
+ SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, HEADER_HEIGHT + _rowHeight));
|
|
|
|
|
|
// Set up the horizontal scrollbar. Turn off AutoShow since we do it manually.
|
|
|
HorizontalScrollBar.AutoShow = false;
|
|
|
@@ -100,18 +104,190 @@ public class CharMap : View, IDesignable
|
|
|
// The scrollbars are in the Padding. VisualRole.Focus/Active are used to draw the
|
|
|
// CharMap headers. Override Padding to force it to draw to match.
|
|
|
Padding!.GettingAttributeForRole += PaddingOnGettingAttributeForRole;
|
|
|
+
|
|
|
+ // Build initial visible rows (all rows with at least one valid codepoint)
|
|
|
+ RebuildVisibleRows ();
|
|
|
}
|
|
|
|
|
|
- private void PaddingOnGettingAttributeForRole (object? sender, VisualRoleEventArgs e)
|
|
|
+ // Visible rows management: each entry is the starting code point of a 16-wide row
|
|
|
+ private readonly List<int> _visibleRowStarts = new ();
|
|
|
+ private readonly Dictionary<int, int> _rowStartToVisibleIndex = new ();
|
|
|
+
|
|
|
+ private void RebuildVisibleRows ()
|
|
|
{
|
|
|
- if (e.Role != VisualRole.Focus && e.Role != VisualRole.Active)
|
|
|
+ _visibleRowStarts.Clear ();
|
|
|
+ _rowStartToVisibleIndex.Clear ();
|
|
|
+
|
|
|
+ int maxRow = MAX_CODE_POINT / 16;
|
|
|
+
|
|
|
+ for (var row = 0; row <= maxRow; row++)
|
|
|
{
|
|
|
- e.Result = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
|
|
|
+ int start = row * 16;
|
|
|
+ bool anyValid = false;
|
|
|
+ bool anyVisible = false;
|
|
|
+
|
|
|
+ for (var col = 0; col < 16; col++)
|
|
|
+ {
|
|
|
+ int cp = start + col;
|
|
|
+ if (cp > RuneExtensions.MaxUnicodeCodePoint)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Rune.IsValid (cp))
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ anyValid = true;
|
|
|
+
|
|
|
+ if (!ShowUnicodeCategory.HasValue)
|
|
|
+ {
|
|
|
+ // With no filter, a row is displayed if it has any valid codepoint
|
|
|
+ anyVisible = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ var rune = new Rune (cp);
|
|
|
+ Span<char> utf16 = new char [2];
|
|
|
+ rune.EncodeToUtf16 (utf16);
|
|
|
+ UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]);
|
|
|
+ if (cat == ShowUnicodeCategory.Value)
|
|
|
+ {
|
|
|
+ anyVisible = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (anyValid && (!ShowUnicodeCategory.HasValue ? anyValid : anyVisible))
|
|
|
+ {
|
|
|
+ _rowStartToVisibleIndex [start] = _visibleRowStarts.Count;
|
|
|
+ _visibleRowStarts.Add (start);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- e.Handled = true;
|
|
|
+ // Update content size to match visible rows
|
|
|
+ SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, _visibleRowStarts.Count * _rowHeight + HEADER_HEIGHT));
|
|
|
+
|
|
|
+ // Keep vertical scrollbar aligned with new content size
|
|
|
+ VerticalScrollBar.ScrollableContentSize = GetContentSize ().Height;
|
|
|
}
|
|
|
|
|
|
+ private int VisibleRowIndexForCodePoint (int codePoint)
|
|
|
+ {
|
|
|
+ int start = (codePoint / 16) * 16;
|
|
|
+ return _rowStartToVisibleIndex.GetValueOrDefault (start, -1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested
|
|
|
+ private int _selectedCodepoint; // Currently selected codepoint
|
|
|
+ private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
|
|
|
+ /// visible.
|
|
|
+ /// </summary>
|
|
|
+ public int SelectedCodePoint
|
|
|
+ {
|
|
|
+ get => _selectedCodepoint;
|
|
|
+ set
|
|
|
+ {
|
|
|
+ if (_selectedCodepoint == value)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT);
|
|
|
+
|
|
|
+ Point offsetToNewCursor = GetCursor (newSelectedCodePoint);
|
|
|
+
|
|
|
+ _selectedCodepoint = newSelectedCodePoint;
|
|
|
+
|
|
|
+ // Ensure the new cursor position is visible
|
|
|
+ ScrollToMakeCursorVisible (offsetToNewCursor);
|
|
|
+
|
|
|
+ SetNeedsDraw ();
|
|
|
+ SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Raised when the selected code point changes.
|
|
|
+ /// </summary>
|
|
|
+ public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets whether the number of columns each glyph is displayed.
|
|
|
+ /// </summary>
|
|
|
+ public bool ShowGlyphWidths
|
|
|
+ {
|
|
|
+ get => _rowHeight == 2;
|
|
|
+ set
|
|
|
+ {
|
|
|
+ _rowHeight = value ? 2 : 1;
|
|
|
+ // height changed => content height depends on row height
|
|
|
+ RebuildVisibleRows ();
|
|
|
+ SetNeedsDraw ();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
|
|
|
+ /// characters.
|
|
|
+ /// </summary>
|
|
|
+ public int StartCodePoint
|
|
|
+ {
|
|
|
+ get => _startCodepoint;
|
|
|
+ set
|
|
|
+ {
|
|
|
+ _startCodepoint = value;
|
|
|
+ SelectedCodePoint = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private UnicodeCategory? _showUnicodeCategory;
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// When set, only glyphs whose UnicodeCategory matches the value are rendered. If <see langword="null"/> (default),
|
|
|
+ /// all glyphs are rendered.
|
|
|
+ /// </summary>
|
|
|
+ public UnicodeCategory? ShowUnicodeCategory
|
|
|
+ {
|
|
|
+ get => _showUnicodeCategory;
|
|
|
+ set
|
|
|
+ {
|
|
|
+ if (_showUnicodeCategory == value)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _showUnicodeCategory = value;
|
|
|
+ RebuildVisibleRows ();
|
|
|
+
|
|
|
+ // Ensure selection is on a visible row
|
|
|
+ int desiredRowStart = (SelectedCodePoint / 16) * 16;
|
|
|
+ if (!_rowStartToVisibleIndex.ContainsKey (desiredRowStart))
|
|
|
+ {
|
|
|
+ // Find nearest visible row (prefer next; fallback to last)
|
|
|
+ int idx = _visibleRowStarts.FindIndex (s => s >= desiredRowStart);
|
|
|
+ if (idx < 0 && _visibleRowStarts.Count > 0)
|
|
|
+ {
|
|
|
+ idx = _visibleRowStarts.Count - 1;
|
|
|
+ }
|
|
|
+ if (idx >= 0)
|
|
|
+ {
|
|
|
+ SelectedCodePoint = _visibleRowStarts [idx];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ SetNeedsDraw ();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
|
|
|
+ private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
|
|
|
+
|
|
|
private bool? Move (ICommandContext? commandContext, int cpOffset)
|
|
|
{
|
|
|
if (RaiseSelecting (commandContext) is true)
|
|
|
@@ -124,6 +300,16 @@ public class CharMap : View, IDesignable
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ private void PaddingOnGettingAttributeForRole (object? sender, VisualRoleEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.Role != VisualRole.Focus && e.Role != VisualRole.Active)
|
|
|
+ {
|
|
|
+ e.Result = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
|
|
|
+ }
|
|
|
+
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+
|
|
|
private void ScrollToMakeCursorVisible (Point offsetToNewCursor)
|
|
|
{
|
|
|
// Adjust vertical scrolling
|
|
|
@@ -147,107 +333,246 @@ public class CharMap : View, IDesignable
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- #region Cursor
|
|
|
+ #region Details Dialog
|
|
|
|
|
|
- private Point GetCursor (int codePoint)
|
|
|
+ [RequiresUnreferencedCode ("AOT")]
|
|
|
+ [RequiresDynamicCode ("AOT")]
|
|
|
+ private void ShowDetails ()
|
|
|
{
|
|
|
- // + 1 for padding between label and first column
|
|
|
- int x = codePoint % 16 * COLUMN_WIDTH + RowLabelWidth + 1 - Viewport.X;
|
|
|
- int y = codePoint / 16 * _rowHeight + HEADER_HEIGHT - Viewport.Y;
|
|
|
+ if (!Application.Initialized)
|
|
|
+ {
|
|
|
+ // Some unit tests invoke Accept without Init
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- return new (x, y);
|
|
|
- }
|
|
|
+ UcdApiClient? client = new ();
|
|
|
+ var decResponse = string.Empty;
|
|
|
+ var getCodePointError = string.Empty;
|
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
- public override Point? PositionCursor ()
|
|
|
- {
|
|
|
- Point cursor = GetCursor (SelectedCodePoint);
|
|
|
+ Dialog? waitIndicator = new ()
|
|
|
+ {
|
|
|
+ Title = Strings.charMapCPInfoDlgTitle,
|
|
|
+ X = Pos.Center (),
|
|
|
+ Y = Pos.Center (),
|
|
|
+ Width = 40,
|
|
|
+ Height = 10,
|
|
|
+ Buttons = [new () { Text = Strings.btnCancel }]
|
|
|
+ };
|
|
|
|
|
|
- if (HasFocus
|
|
|
- && cursor.X >= RowLabelWidth
|
|
|
- && cursor.X < Viewport.Width
|
|
|
- && cursor.Y > 0
|
|
|
- && cursor.Y < Viewport.Height)
|
|
|
+ var errorLabel = new Label
|
|
|
{
|
|
|
- Move (cursor.X, cursor.Y);
|
|
|
+ Text = UcdApiClient.BaseUrl,
|
|
|
+ X = 0,
|
|
|
+ Y = 0,
|
|
|
+ Width = Dim.Fill (),
|
|
|
+ Height = Dim.Fill (3),
|
|
|
+ TextAlignment = Alignment.Center
|
|
|
+ };
|
|
|
+
|
|
|
+ var spinner = new SpinnerView
|
|
|
+ {
|
|
|
+ X = Pos.Center (),
|
|
|
+ Y = Pos.Bottom (errorLabel),
|
|
|
+ Style = new SpinnerStyle.Aesthetic ()
|
|
|
+ };
|
|
|
+ spinner.AutoSpin = true;
|
|
|
+ waitIndicator.Add (errorLabel);
|
|
|
+ waitIndicator.Add (spinner);
|
|
|
+
|
|
|
+ waitIndicator.Ready += async (s, a) =>
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false);
|
|
|
+ Application.Invoke (() => waitIndicator.RequestStop ());
|
|
|
+ }
|
|
|
+ catch (HttpRequestException e)
|
|
|
+ {
|
|
|
+ getCodePointError = errorLabel.Text = e.Message;
|
|
|
+ Application.Invoke (() => waitIndicator.RequestStop ());
|
|
|
+ }
|
|
|
+ };
|
|
|
+ Application.Run (waitIndicator);
|
|
|
+ waitIndicator.Dispose ();
|
|
|
+
|
|
|
+ var name = string.Empty;
|
|
|
+
|
|
|
+ if (!string.IsNullOrEmpty (decResponse))
|
|
|
+ {
|
|
|
+ using JsonDocument document = JsonDocument.Parse (decResponse);
|
|
|
+
|
|
|
+ JsonElement root = document.RootElement;
|
|
|
+
|
|
|
+ // Get a property by name and output its value
|
|
|
+ if (root.TryGetProperty ("name", out JsonElement nameElement))
|
|
|
+ {
|
|
|
+ name = nameElement.GetString ();
|
|
|
+ }
|
|
|
+
|
|
|
+ decResponse = JsonSerializer.Serialize (
|
|
|
+ document.RootElement,
|
|
|
+ new
|
|
|
+ JsonSerializerOptions
|
|
|
+ { WriteIndented = true }
|
|
|
+ );
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- return null;
|
|
|
+ decResponse = getCodePointError;
|
|
|
}
|
|
|
|
|
|
- return cursor;
|
|
|
- }
|
|
|
+ var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
|
|
|
|
|
|
- #endregion Cursor
|
|
|
+ Button? copyGlyph = new () { Text = Strings.charMapCopyGlyph };
|
|
|
+ Button? copyCodepoint = new () { Text = Strings.charMapCopyCP };
|
|
|
+ Button? cancel = new () { Text = Strings.btnCancel };
|
|
|
|
|
|
- // ReSharper disable once InconsistentNaming
|
|
|
- private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
|
|
|
- private int _selectedCodepoint; // Currently selected codepoint
|
|
|
- private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport
|
|
|
+ var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
|
|
|
- /// visible.
|
|
|
- /// </summary>
|
|
|
- public int SelectedCodePoint
|
|
|
- {
|
|
|
- get => _selectedCodepoint;
|
|
|
- set
|
|
|
- {
|
|
|
- if (_selectedCodepoint == value)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
+ copyGlyph.Accepting += (s, a) =>
|
|
|
+ {
|
|
|
+ CopyGlyph ();
|
|
|
+ dlg!.RequestStop ();
|
|
|
+ a.Handled = true;
|
|
|
+ };
|
|
|
|
|
|
- int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT);
|
|
|
+ copyCodepoint.Accepting += (s, a) =>
|
|
|
+ {
|
|
|
+ CopyCodePoint ();
|
|
|
+ dlg!.RequestStop ();
|
|
|
+ a.Handled = true;
|
|
|
+ };
|
|
|
|
|
|
- Point offsetToNewCursor = GetCursor (newSelectedCodePoint);
|
|
|
+ cancel.Accepting += (s, a) =>
|
|
|
+ {
|
|
|
+ dlg!.RequestStop ();
|
|
|
+ a.Handled = true;
|
|
|
+ };
|
|
|
+
|
|
|
+ var rune = (Rune)SelectedCodePoint;
|
|
|
+ var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new () { Text = "Category: ", X = 0, Y = Pos.Bottom (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+ Span<char> utf16 = stackalloc char [2];
|
|
|
+ int charCount = rune.EncodeToUtf16 (utf16);
|
|
|
+
|
|
|
+ // Get the bidi class for the first code unit
|
|
|
+ // For most bidi characters, the first code unit is sufficient
|
|
|
+ UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]);
|
|
|
+
|
|
|
+ label = new () { Text = $"{category}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
+ dlg.Add (label);
|
|
|
+
|
|
|
+ label = new ()
|
|
|
+ {
|
|
|
+ Text =
|
|
|
+ $"{Strings.charMapInfoDlgInfoLabel} {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
|
|
|
+ X = 0,
|
|
|
+ Y = Pos.Bottom (label)
|
|
|
+ };
|
|
|
+ dlg.Add (label);
|
|
|
|
|
|
- _selectedCodepoint = newSelectedCodePoint;
|
|
|
+ var json = new TextView
|
|
|
+ {
|
|
|
+ X = 0,
|
|
|
+ Y = Pos.Bottom (label),
|
|
|
+ Width = Dim.Fill (),
|
|
|
+ Height = Dim.Fill (2),
|
|
|
+ ReadOnly = true,
|
|
|
+ Text = decResponse
|
|
|
+ };
|
|
|
|
|
|
- // Ensure the new cursor position is visible
|
|
|
- ScrollToMakeCursorVisible (offsetToNewCursor);
|
|
|
+ dlg.Add (json);
|
|
|
|
|
|
- SetNeedsDraw ();
|
|
|
- SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint));
|
|
|
- }
|
|
|
+ Application.Run (dlg);
|
|
|
+ dlg.Dispose ();
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Raised when the selected code point changes.
|
|
|
- /// </summary>
|
|
|
- public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
|
|
|
+ #endregion Details Dialog
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
|
|
|
- /// characters.
|
|
|
- /// </summary>
|
|
|
- public int StartCodePoint
|
|
|
+ #region Cursor
|
|
|
+
|
|
|
+ private Point GetCursor (int codePoint)
|
|
|
{
|
|
|
- get => _startCodepoint;
|
|
|
- set
|
|
|
+ // + 1 for padding between label and first column
|
|
|
+ int x = codePoint % 16 * COLUMN_WIDTH + RowLabelWidth + 1 - Viewport.X;
|
|
|
+
|
|
|
+ int visibleRowIndex = VisibleRowIndexForCodePoint (codePoint);
|
|
|
+ if (visibleRowIndex < 0)
|
|
|
{
|
|
|
- _startCodepoint = value;
|
|
|
- SelectedCodePoint = value;
|
|
|
+ // If filtered out, stick to current Y to avoid jumping; caller will clamp
|
|
|
+ int fallbackY = HEADER_HEIGHT - Viewport.Y;
|
|
|
+ return new (x, fallbackY);
|
|
|
}
|
|
|
+
|
|
|
+ int y = visibleRowIndex * _rowHeight + HEADER_HEIGHT - Viewport.Y;
|
|
|
+
|
|
|
+ return new (x, y);
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Gets or sets whether the number of columns each glyph is displayed.
|
|
|
- /// </summary>
|
|
|
- public bool ShowGlyphWidths
|
|
|
+ /// <inheritdoc/>
|
|
|
+ public override Point? PositionCursor ()
|
|
|
{
|
|
|
- get => _rowHeight == 2;
|
|
|
- set
|
|
|
+ Point cursor = GetCursor (SelectedCodePoint);
|
|
|
+
|
|
|
+ if (HasFocus
|
|
|
+ && cursor.X >= RowLabelWidth
|
|
|
+ && cursor.X < Viewport.Width
|
|
|
+ && cursor.Y > 0
|
|
|
+ && cursor.Y < Viewport.Height)
|
|
|
{
|
|
|
- _rowHeight = value ? 2 : 1;
|
|
|
- SetNeedsDraw ();
|
|
|
+ Move (cursor.X, cursor.Y);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ return null;
|
|
|
}
|
|
|
+
|
|
|
+ return cursor;
|
|
|
}
|
|
|
|
|
|
- private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
|
|
|
- private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
|
|
|
+ #endregion Cursor
|
|
|
|
|
|
#region Drawing
|
|
|
|
|
|
@@ -262,7 +587,7 @@ public class CharMap : View, IDesignable
|
|
|
}
|
|
|
|
|
|
int selectedCol = SelectedCodePoint % 16;
|
|
|
- int selectedRow = SelectedCodePoint / 16;
|
|
|
+ int selectedRowIndex = VisibleRowIndexForCodePoint (SelectedCodePoint);
|
|
|
|
|
|
// Headers
|
|
|
|
|
|
@@ -302,32 +627,33 @@ public class CharMap : View, IDesignable
|
|
|
// Start at 1 because Header.
|
|
|
for (var y = 1; y < Viewport.Height; y++)
|
|
|
{
|
|
|
- // What row is this?
|
|
|
- int row = (y + Viewport.Y - 1) / _rowHeight;
|
|
|
- int val = row * 16;
|
|
|
+ // Which visible row is this?
|
|
|
+ int visibleRow = (y + Viewport.Y - 1) / _rowHeight;
|
|
|
+
|
|
|
+ if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count)
|
|
|
+ {
|
|
|
+ // No row at this y; clear label area and continue
|
|
|
+ Move (0, y);
|
|
|
+ AddStr (new (' ', Viewport.Width));
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ int rowStart = _visibleRowStarts [visibleRow];
|
|
|
|
|
|
// Draw the row label (U+XXXX_)
|
|
|
SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
|
|
|
Move (0, y);
|
|
|
|
|
|
// Swap Active/Focus so the selected row is highlighted
|
|
|
- if (y + Viewport.Y - 1 == selectedRow)
|
|
|
+ if (visibleRow == selectedRowIndex)
|
|
|
{
|
|
|
SetAttributeForRole (HasFocus ? VisualRole.Active : VisualRole.Focus);
|
|
|
}
|
|
|
|
|
|
- if (val > MAX_CODE_POINT)
|
|
|
- {
|
|
|
- // No row
|
|
|
- Move (0, y);
|
|
|
- AddStr (new (' ', RowLabelWidth));
|
|
|
-
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
|
|
|
{
|
|
|
- AddStr ($"U+{val / 16:x5}_");
|
|
|
+ AddStr ($"U+{rowStart / 16:x5}_");
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
@@ -349,12 +675,24 @@ public class CharMap : View, IDesignable
|
|
|
Move (x, y);
|
|
|
|
|
|
// If we're at the cursor position highlight the cell
|
|
|
- if (row == selectedRow && col == selectedCol)
|
|
|
+ if (visibleRow == selectedRowIndex && col == selectedCol)
|
|
|
{
|
|
|
SetAttributeForRole (VisualRole.Active);
|
|
|
}
|
|
|
|
|
|
- int scalar = val + col;
|
|
|
+ int scalar = rowStart + col;
|
|
|
+
|
|
|
+ // Don't render out-of-range scalars
|
|
|
+ if (scalar > MAX_CODE_POINT)
|
|
|
+ {
|
|
|
+ AddRune (' ');
|
|
|
+ if (visibleRow == selectedRowIndex && col == selectedCol)
|
|
|
+ {
|
|
|
+ SetAttributeForRole (VisualRole.Normal);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
var rune = (Rune)'?';
|
|
|
|
|
|
if (Rune.IsValid (scalar))
|
|
|
@@ -364,9 +702,88 @@ public class CharMap : View, IDesignable
|
|
|
|
|
|
int width = rune.GetColumns ();
|
|
|
|
|
|
+ // Compute visibility based on ShowUnicodeCategory
|
|
|
+ bool isVisible = Rune.IsValid (scalar);
|
|
|
+ if (isVisible && ShowUnicodeCategory.HasValue)
|
|
|
+ {
|
|
|
+ Span<char> filterUtf16 = new char [2];
|
|
|
+ rune.EncodeToUtf16 (filterUtf16);
|
|
|
+ UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (filterUtf16 [0]);
|
|
|
+ isVisible = cat == ShowUnicodeCategory.Value;
|
|
|
+ }
|
|
|
+
|
|
|
if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
|
|
|
{
|
|
|
- // Draw the rune
|
|
|
+ // Glyph row
|
|
|
+ if (isVisible)
|
|
|
+ {
|
|
|
+ RenderRune (rune, width);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ AddRune (' ');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Width row (ShowGlyphWidths)
|
|
|
+ if (isVisible)
|
|
|
+ {
|
|
|
+ // Draw the width of the rune faint
|
|
|
+ Attribute attr = GetAttributeForRole (VisualRole.Normal);
|
|
|
+ SetAttribute (attr with { Style = attr.Style | TextStyle.Faint });
|
|
|
+ AddStr ($"{width}");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ AddRune (' ');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // If we're at the cursor position, and we don't have focus
|
|
|
+ if (visibleRow == selectedRowIndex && col == selectedCol)
|
|
|
+ {
|
|
|
+ SetAttributeForRole (VisualRole.Normal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+ void RenderRune (Rune rune, int width)
|
|
|
+ {
|
|
|
+ // Get the UnicodeCategory
|
|
|
+ Span<char> utf16 = new char [2];
|
|
|
+ int charCount = rune.EncodeToUtf16 (utf16);
|
|
|
+
|
|
|
+ // Get the bidi class for the first code unit
|
|
|
+ // For most bidi characters, the first code unit is sufficient
|
|
|
+ UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]);
|
|
|
+
|
|
|
+ switch (category)
|
|
|
+ {
|
|
|
+ case UnicodeCategory.OtherNotAssigned:
|
|
|
+ SetAttributeForRole (VisualRole.Highlight);
|
|
|
+ AddRune (Rune.ReplacementChar);
|
|
|
+ SetAttributeForRole (VisualRole.Normal);
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ // Format character that affects the layout of text or the operation of text processes, but is not normally rendered.
|
|
|
+ // These report width of 0 and don't render on their own.
|
|
|
+ case UnicodeCategory.Format:
|
|
|
+ SetAttributeForRole (VisualRole.Highlight);
|
|
|
+ AddRune ('F');
|
|
|
+ SetAttributeForRole (VisualRole.Normal);
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ // Nonspacing character that indicates modifications of a base character.
|
|
|
+ case UnicodeCategory.NonSpacingMark:
|
|
|
+ // Spacing character that indicates modifications of a base character and affects the width of the glyph for that base character.
|
|
|
+ case UnicodeCategory.SpacingCombiningMark:
|
|
|
+ // Enclosing mark character, which is a nonspacing combining character that surrounds all previous characters up to and including a base character.
|
|
|
+ case UnicodeCategory.EnclosingMark:
|
|
|
if (width > 0)
|
|
|
{
|
|
|
AddRune (rune);
|
|
|
@@ -394,28 +811,39 @@ public class CharMap : View, IDesignable
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- AddRune (Rune.ReplacementChar);
|
|
|
+ SetAttributeForRole (VisualRole.Highlight);
|
|
|
+ AddRune ('M');
|
|
|
+ SetAttributeForRole (VisualRole.Normal);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- // Draw the width of the rune faint
|
|
|
- Attribute attr = GetAttributeForRole (VisualRole.Normal);
|
|
|
- SetAttribute (attr with { Style = attr.Style | TextStyle.Faint });
|
|
|
- AddStr ($"{width}");
|
|
|
- }
|
|
|
|
|
|
- // If we're at the cursor position, and we don't have focus
|
|
|
- if (row == selectedRow && col == selectedCol)
|
|
|
- {
|
|
|
- SetAttributeForRole (VisualRole.Normal);
|
|
|
- }
|
|
|
+ break;
|
|
|
+
|
|
|
+ // These report width of 0, but render as 1
|
|
|
+ case UnicodeCategory.Control:
|
|
|
+ case UnicodeCategory.LineSeparator:
|
|
|
+ case UnicodeCategory.ParagraphSeparator:
|
|
|
+ case UnicodeCategory.Surrogate:
|
|
|
+ AddRune (rune);
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+
|
|
|
+ // Draw the rune
|
|
|
+ if (width > 0)
|
|
|
+ {
|
|
|
+ AddRune (rune);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ throw new InvalidOperationException ($"The Rune \"{rune}\" (U+{rune.Value:x6}) has zero width and no special-case UnicodeCategory logic applies.");
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- return true;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -560,7 +988,14 @@ public class CharMap : View, IDesignable
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- int row = (position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header
|
|
|
+ int visibleRow = (position.Y - 1 - -Viewport.Y) / _rowHeight;
|
|
|
+
|
|
|
+ if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count)
|
|
|
+ {
|
|
|
+ codePoint = 0;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
int col = (position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
|
|
|
|
|
|
if (col > 15)
|
|
|
@@ -568,7 +1003,7 @@ public class CharMap : View, IDesignable
|
|
|
col = 15;
|
|
|
}
|
|
|
|
|
|
- codePoint = row * 16 + col;
|
|
|
+ codePoint = _visibleRowStarts [visibleRow] + col;
|
|
|
|
|
|
if (codePoint > MAX_CODE_POINT)
|
|
|
{
|
|
|
@@ -579,199 +1014,4 @@ public class CharMap : View, IDesignable
|
|
|
}
|
|
|
|
|
|
#endregion Mouse Handling
|
|
|
-
|
|
|
- #region Details Dialog
|
|
|
-
|
|
|
- [RequiresUnreferencedCode ("AOT")]
|
|
|
- [RequiresDynamicCode ("AOT")]
|
|
|
- private void ShowDetails ()
|
|
|
- {
|
|
|
- if (!Application.Initialized)
|
|
|
- {
|
|
|
- // Some unit tests invoke Accept without Init
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- UcdApiClient? client = new ();
|
|
|
- var decResponse = string.Empty;
|
|
|
- var getCodePointError = string.Empty;
|
|
|
-
|
|
|
- Dialog? waitIndicator = new ()
|
|
|
- {
|
|
|
- Title = Strings.charMapCPInfoDlgTitle,
|
|
|
- X = Pos.Center (),
|
|
|
- Y = Pos.Center (),
|
|
|
- Width = 40,
|
|
|
- Height = 10,
|
|
|
- Buttons = [new () { Text = Strings.btnCancel }]
|
|
|
- };
|
|
|
-
|
|
|
- var errorLabel = new Label
|
|
|
- {
|
|
|
- Text = UcdApiClient.BaseUrl,
|
|
|
- X = 0,
|
|
|
- Y = 0,
|
|
|
- Width = Dim.Fill (),
|
|
|
- Height = Dim.Fill (3),
|
|
|
- TextAlignment = Alignment.Center
|
|
|
- };
|
|
|
-
|
|
|
- var spinner = new SpinnerView
|
|
|
- {
|
|
|
- X = Pos.Center (),
|
|
|
- Y = Pos.Bottom (errorLabel),
|
|
|
- Style = new SpinnerStyle.Aesthetic ()
|
|
|
- };
|
|
|
- spinner.AutoSpin = true;
|
|
|
- waitIndicator.Add (errorLabel);
|
|
|
- waitIndicator.Add (spinner);
|
|
|
-
|
|
|
- waitIndicator.Ready += async (s, a) =>
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false);
|
|
|
- Application.Invoke (() => waitIndicator.RequestStop ());
|
|
|
- }
|
|
|
- catch (HttpRequestException e)
|
|
|
- {
|
|
|
- getCodePointError = errorLabel.Text = e.Message;
|
|
|
- Application.Invoke (() => waitIndicator.RequestStop ());
|
|
|
- }
|
|
|
- };
|
|
|
- Application.Run (waitIndicator);
|
|
|
- waitIndicator.Dispose ();
|
|
|
-
|
|
|
- if (!string.IsNullOrEmpty (decResponse))
|
|
|
- {
|
|
|
- var name = string.Empty;
|
|
|
-
|
|
|
- using (JsonDocument document = JsonDocument.Parse (decResponse))
|
|
|
- {
|
|
|
- JsonElement root = document.RootElement;
|
|
|
-
|
|
|
- // Get a property by name and output its value
|
|
|
- if (root.TryGetProperty ("name", out JsonElement nameElement))
|
|
|
- {
|
|
|
- name = nameElement.GetString ();
|
|
|
- }
|
|
|
-
|
|
|
- //// Navigate to a nested property and output its value
|
|
|
- //if (root.TryGetProperty ("property3", out JsonElement property3Element)
|
|
|
- //&& property3Element.TryGetProperty ("nestedProperty", out JsonElement nestedPropertyElement)) {
|
|
|
- // Console.WriteLine (nestedPropertyElement.GetString ());
|
|
|
- //}
|
|
|
- decResponse = JsonSerializer.Serialize (
|
|
|
- document.RootElement,
|
|
|
- new
|
|
|
- JsonSerializerOptions
|
|
|
- { WriteIndented = true }
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
|
|
|
-
|
|
|
- Button? copyGlyph = new () { Text = Strings.charMapCopyGlyph };
|
|
|
- Button? copyCodepoint = new () { Text = Strings.charMapCopyCP };
|
|
|
- Button? cancel = new () { Text = Strings.btnCancel };
|
|
|
-
|
|
|
- var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
|
|
|
-
|
|
|
- copyGlyph.Accepting += (s, a) =>
|
|
|
- {
|
|
|
- CopyGlyph ();
|
|
|
- dlg!.RequestStop ();
|
|
|
- a.Handled = true;
|
|
|
- };
|
|
|
-
|
|
|
- copyCodepoint.Accepting += (s, a) =>
|
|
|
- {
|
|
|
- CopyCodePoint ();
|
|
|
- dlg!.RequestStop ();
|
|
|
- a.Handled = true;
|
|
|
- };
|
|
|
- cancel.Accepting += (s, a) =>
|
|
|
- {
|
|
|
- dlg!.RequestStop ();
|
|
|
- a.Handled = true;
|
|
|
- };
|
|
|
-
|
|
|
- var rune = (Rune)SelectedCodePoint;
|
|
|
- var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new () { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- label = new ()
|
|
|
- {
|
|
|
- Text =
|
|
|
- $"{Strings.charMapInfoDlgInfoLabel} {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
|
|
|
- X = 0,
|
|
|
- Y = Pos.Bottom (label)
|
|
|
- };
|
|
|
- dlg.Add (label);
|
|
|
-
|
|
|
- var json = new TextView
|
|
|
- {
|
|
|
- X = 0,
|
|
|
- Y = Pos.Bottom (label),
|
|
|
- Width = Dim.Fill (),
|
|
|
- Height = Dim.Fill (2),
|
|
|
- ReadOnly = true,
|
|
|
- Text = decResponse
|
|
|
- };
|
|
|
-
|
|
|
- dlg.Add (json);
|
|
|
-
|
|
|
- Application.Run (dlg);
|
|
|
- dlg.Dispose ();
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- MessageBox.ErrorQuery (
|
|
|
- Strings.error,
|
|
|
- $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} {Strings.failedGetting}{Environment.NewLine}{new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
|
|
|
- Strings.btnOk
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- #endregion Details Dialog
|
|
|
}
|