Merge branch 'v2_develop' into copilot/fix-e6dde989-9ea1-4d83-8522-54ed8f70815a

This commit is contained in:
Tig
2025-09-27 20:28:20 +01:00
committed by GitHub
7 changed files with 725 additions and 376 deletions

View File

@@ -1,9 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace UICatalog.Scenarios;
@@ -24,6 +21,33 @@ public class CharacterMap : Scenario
private Label? _errorLabel;
private TableView? _categoryList;
private CharMap? _charMap;
private OptionSelector? _unicodeCategorySelector;
public override List<Key> GetDemoKeyStrokes ()
{
List<Key> keys = new ();
for (var i = 0; i < 200; i++)
{
keys.Add (Key.CursorDown);
}
// Category table
keys.Add (Key.Tab.WithShift);
// Block elements
keys.Add (Key.B);
keys.Add (Key.L);
keys.Add (Key.Tab);
for (var i = 0; i < 200; i++)
{
keys.Add (Key.CursorLeft);
}
return keys;
}
// Don't create a Window, just return the top-level view
public override void Main ()
@@ -39,9 +63,9 @@ public class CharacterMap : Scenario
{
X = 0,
Y = 1,
Height = Dim.Fill (),
// SchemeName = "Base"
Height = Dim.Fill ()
// SchemeName = "Base"
};
top.Add (_charMap);
@@ -50,7 +74,8 @@ public class CharacterMap : Scenario
X = Pos.Right (_charMap) + 1,
Y = Pos.Y (_charMap),
HotKeySpecifier = (Rune)'_',
Text = "_Jump To:",
Text = "_Jump To:"
//SchemeName = "Dialog"
};
top.Add (jumpLabel);
@@ -60,7 +85,8 @@ public class CharacterMap : Scenario
X = Pos.Right (jumpLabel) + 1,
Y = Pos.Y (_charMap),
Width = 17,
Caption = "e.g. 01BE3 or ✈",
Caption = "e.g. 01BE3 or ✈"
//SchemeName = "Dialog"
};
top.Add (jumpEdit);
@@ -89,10 +115,12 @@ public class CharacterMap : Scenario
jumpEdit.Accepting += JumpEditOnAccept;
_categoryList = new () {
X = Pos.Right (_charMap),
Y = Pos.Bottom (jumpLabel),
Height = Dim.Fill (),
_categoryList = new ()
{
X = Pos.Right (_charMap),
Y = Pos.Bottom (jumpLabel),
Height = Dim.Fill ()
//SchemeName = "Dialog"
};
_categoryList.FullRowSelect = true;
@@ -165,7 +193,7 @@ public class CharacterMap : Scenario
),
new (
"_Options",
new MenuItemv2 [] { CreateMenuShowWidth () }
[CreateMenuShowWidth (), CreateMenuUnicodeCategorySelector ()]
)
]
};
@@ -317,6 +345,7 @@ public class CharacterMap : Scenario
CheckedState = _charMap!.ShowGlyphWidths ? CheckState.Checked : CheckState.None
};
var item = new MenuItemv2 { CommandView = cb };
item.Action += () =>
{
if (_charMap is { })
@@ -328,29 +357,48 @@ public class CharacterMap : Scenario
return item;
}
public override List<Key> GetDemoKeyStrokes ()
private MenuItemv2 CreateMenuUnicodeCategorySelector ()
{
List<Key> keys = new ();
// First option is "All" (no filter), followed by all UnicodeCategory names
string [] allCategoryNames = Enum.GetNames<UnicodeCategory> ();
var options = new string [allCategoryNames.Length + 1];
options [0] = "All";
Array.Copy (allCategoryNames, 0, options, 1, allCategoryNames.Length);
for (var i = 0; i < 200; i++)
// TODO: When #4126 is merged update this to use OptionSelector<UnicodeCategory?>
var selector = new OptionSelector
{
keys.Add (Key.CursorDown);
}
AssignHotKeysToCheckBoxes = true,
Options = options
};
// Category table
keys.Add (Key.Tab.WithShift);
_unicodeCategorySelector = selector;
// Block elements
keys.Add (Key.B);
keys.Add (Key.L);
// Default to "All"
selector.SelectedItem = 0;
_charMap!.ShowUnicodeCategory = null;
keys.Add (Key.Tab);
selector.SelectedItemChanged += (s, e) =>
{
int? idx = selector.SelectedItem;
for (var i = 0; i < 200; i++)
{
keys.Add (Key.CursorLeft);
}
if (idx is null)
{
return;
}
return keys;
if (idx.Value == 0)
{
_charMap.ShowUnicodeCategory = null;
}
else
{
// Map index to UnicodeCategory (offset by 1 because 0 is "All")
UnicodeCategory cat = Enum.GetValues<UnicodeCategory> () [idx.Value - 1];
_charMap.ShowUnicodeCategory = cat;
}
};
return new() { CommandView = selector };
}
}

View File

@@ -11,22 +11,78 @@ public class CombiningMarks : Scenario
var top = new Toplevel ();
top.DrawComplete += (s, e) =>
{
top.Move (0, 0);
top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616.");
top.Move (0, 2);
top.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr.");
top.Move (0, 3);
top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr.");
top.Move (0, 4);
top.AddRune ('[');
top.AddRune ('a');
top.AddRune ('\u0301');
top.AddRune ('\u0301');
top.AddRune ('\u0328');
top.AddRune (']');
top.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each.");
};
{
// Forces reset _lineColsOffset because we're dealing with direct draw
Application.ClearScreenNextIteration = true;
var i = -1;
top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616.");
top.Move (0, ++i);
top.AddStr ("\u0301<- \"\\u0301\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\u0301]<- \"[\\u0301]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[ \u0301]<- \"[ \\u0301]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\u0301 ]<- \"[\\u0301 ]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("\u0301\u0301\u0328<- \"\\u0301\\u0301\\u0328\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\u0301\u0301\u0328]<- \"[\\u0301\\u0301\\u0328]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr.");
top.Move (0, ++i);
top.AddRune ('[');
top.AddRune ('a');
top.AddRune ('\u0301');
top.AddRune ('\u0301');
top.AddRune ('\u0328');
top.AddRune (']');
top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each.");
top.Move (0, ++i);
top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("\u00ad<- \"\\u00ad\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\u00ad]<- \"[\\u00ad]\" using AddStr.");
top.Move (0, ++i);
top.AddRune ('[');
top.AddRune ('\u00ad');
top.AddRune (']');
top.AddStr ("<- \"[\\u00ad]\" using AddRune for each.");
i++;
top.Move (0, ++i);
top.AddStr ("From now on we are using TextFormatter");
TextFormatter tf = new () { Text = "[e\u0301\u0301\u0328]<- \"[e\\u0301\\u0301\\u0328]\" using TextFormatter." };
tf.Draw (new (0, ++i, tf.Text.Length, 1), top.GetAttributeForRole (VisualRole.Normal), top.GetAttributeForRole (VisualRole.Normal));
tf.Text = "[e\u0328\u0301]<- \"[e\\u0328\\u0301]\" using TextFormatter.";
tf.Draw (new (0, ++i, tf.Text.Length, 1), top.GetAttributeForRole (VisualRole.Normal), top.GetAttributeForRole (VisualRole.Normal));
i++;
top.Move (0, ++i);
top.AddStr ("From now on we are using Surrogate pairs with combining diacritics");
top.Move (0, ++i);
top.AddStr ("[\ud835\udc4b\u0302]<- \"[\\ud835\\udc4b\\u0302]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\ud83d\udc68\ud83e\uddd2]<- \"[\\ud83d\\udc68\\ud83e\\uddd2]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("\u200d<- \"\\u200d\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\u200d]<- \"[\\u200d]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\ud83d\udc68\u200d\ud83e\uddd2]<- \"[\\ud83d\\udc68\\u200d\\ud83e\\uddd2]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\U0001F469\U0001F9D2]<- \"[\\U0001F469\\U0001F9D2]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F469\\u200D\\U0001F9D2]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\U0001F468\U0001F469\U0001F9D2]<- \"[\\U0001F468\\U0001F469\\U0001F9D2]\" using AddStr.");
top.Move (0, ++i);
top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F9D2]\" using AddStr.");
};
Application.Run (top);
top.Dispose ();

View File

@@ -82,16 +82,6 @@ public static partial class Application // Initialization (Init/Shutdown)
if (driver is { })
{
Driver = driver;
if (driver is FakeDriver)
{
//// We're running unit tests. Disable loading config files other than default
//if (Locations == ConfigLocations.All)
//{
// Locations = ConfigLocations.Default;
// ResetAllSettings ();
//}
}
}
// Ignore Configuration for ForceDriver if driverName is specified

View File

@@ -111,7 +111,22 @@ public static class RuneExtensions
/// The number of columns required to fit the rune, 0 if the argument is the null character, or -1 if the value is
/// not printable, otherwise the number of columns that the rune occupies.
/// </returns>
public static int GetColumns (this Rune rune) { return UnicodeCalculator.GetWidth (rune); }
public static int GetColumns (this Rune rune)
{
int value = rune.Value;
// TODO: Remove this code when #4259 is fixed
// TODO: See https://github.com/gui-cs/Terminal.Gui/issues/4259
if (value is >= 0x2630 and <= 0x2637 || // Trigrams
value is >= 0x268A and <= 0x268F || // Monograms/Digrams
value is >= 0x4DC0 and <= 0x4DFF) // Hexagrams
{
return 2; // Assume double-width due to Windows Terminal font rendering
}
// Fallback to original GetWidth for other code points
return UnicodeCalculator.GetWidth (rune);
}
/// <summary>Get number of bytes required to encode the rune, based on the provided encoding.</summary>
/// <remarks>This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.</remarks>

View File

@@ -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,89 +104,82 @@ 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 bool? Move (ICommandContext? commandContext, int cpOffset)
private int VisibleRowIndexForCodePoint (int codePoint)
{
if (RaiseSelecting (commandContext) is true)
{
return true;
}
SelectedCodePoint += cpOffset;
return true;
int start = (codePoint / 16) * 16;
return _rowStartToVisibleIndex.GetValueOrDefault (start, -1);
}
private void ScrollToMakeCursorVisible (Point offsetToNewCursor)
{
// Adjust vertical scrolling
if (offsetToNewCursor.Y < 1) // Header is at Y = 0
{
ScrollVertical (offsetToNewCursor.Y - HEADER_HEIGHT);
}
else if (offsetToNewCursor.Y >= Viewport.Height)
{
ScrollVertical (offsetToNewCursor.Y - Viewport.Height + HEADER_HEIGHT);
}
// Adjust horizontal scrolling
if (offsetToNewCursor.X < RowLabelWidth + 1)
{
ScrollHorizontal (offsetToNewCursor.X - (RowLabelWidth + 1));
}
else if (offsetToNewCursor.X >= Viewport.Width)
{
ScrollHorizontal (offsetToNewCursor.X - Viewport.Width + 1);
}
}
#region Cursor
private Point GetCursor (int codePoint)
{
// + 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;
return new (x, y);
}
/// <inheritdoc/>
public override Point? PositionCursor ()
{
Point cursor = GetCursor (SelectedCodePoint);
if (HasFocus
&& cursor.X >= RowLabelWidth
&& cursor.X < Viewport.Width
&& cursor.Y > 0
&& cursor.Y < Viewport.Height)
{
Move (cursor.X, cursor.Y);
}
else
{
return null;
}
return cursor;
}
#endregion Cursor
// ReSharper disable once InconsistentNaming
private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
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
@@ -219,6 +216,21 @@ public class CharMap : View, IDesignable
/// </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.
@@ -233,15 +245,42 @@ public class CharMap : View, IDesignable
}
}
private UnicodeCategory? _showUnicodeCategory;
/// <summary>
/// Gets or sets whether the number of columns each glyph is displayed.
/// When set, only glyphs whose UnicodeCategory matches the value are rendered. If <see langword="null"/> (default),
/// all glyphs are rendered.
/// </summary>
public bool ShowGlyphWidths
public UnicodeCategory? ShowUnicodeCategory
{
get => _rowHeight == 2;
get => _showUnicodeCategory;
set
{
_rowHeight = value ? 2 : 1;
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 ();
}
}
@@ -249,6 +288,292 @@ public class CharMap : View, IDesignable
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)
{
return true;
}
SelectedCodePoint += cpOffset;
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
if (offsetToNewCursor.Y < 1) // Header is at Y = 0
{
ScrollVertical (offsetToNewCursor.Y - HEADER_HEIGHT);
}
else if (offsetToNewCursor.Y >= Viewport.Height)
{
ScrollVertical (offsetToNewCursor.Y - Viewport.Height + HEADER_HEIGHT);
}
// Adjust horizontal scrolling
if (offsetToNewCursor.X < RowLabelWidth + 1)
{
ScrollHorizontal (offsetToNewCursor.X - (RowLabelWidth + 1));
}
else if (offsetToNewCursor.X >= Viewport.Width)
{
ScrollHorizontal (offsetToNewCursor.X - Viewport.Width + 1);
}
}
#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 ();
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
{
decResponse = getCodePointError;
}
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 = "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);
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 ();
}
#endregion Details Dialog
#region Cursor
private Point GetCursor (int codePoint)
{
// + 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)
{
// 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);
}
/// <inheritdoc/>
public override Point? PositionCursor ()
{
Point cursor = GetCursor (SelectedCodePoint);
if (HasFocus
&& cursor.X >= RowLabelWidth
&& cursor.X < Viewport.Width
&& cursor.Y > 0
&& cursor.Y < Viewport.Height)
{
Move (cursor.X, cursor.Y);
}
else
{
return null;
}
return cursor;
}
#endregion Cursor
#region Drawing
private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
@@ -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
}

View File

@@ -515,7 +515,7 @@ public class TextView : View, IDesignable
Command.Context,
() =>
{
ShowContextMenu (true);
ShowContextMenu (null);
return true;
}
@@ -1745,13 +1745,7 @@ public class TextView : View, IDesignable
}
else if (ev.Flags == ContextMenu!.MouseFlags)
{
ContextMenu!.X = ev.ScreenPosition.X;
ContextMenu!.Y = ev.ScreenPosition.Y;
ShowContextMenu (false);
//ContextMenu.Position = ViewportToScreen ((Viewport with { X = ev.Position.X, Y = ev.Position.Y }).Location);
//ShowContextMenu ();
ShowContextMenu (ev.ScreenPosition);
}
OnUnwrappedCursorPosition ();
@@ -4574,14 +4568,18 @@ public class TextView : View, IDesignable
}
}
private void ShowContextMenu (bool keyboard)
private void ShowContextMenu (Point? mousePosition)
{
if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture))
{
_currentCulture = Thread.CurrentThread.CurrentUICulture;
}
ContextMenu?.MakeVisible (ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y)));
if (mousePosition is null)
{
mousePosition = ViewportToScreen (new Point (CursorPosition.X, CursorPosition.Y));
}
ContextMenu?.MakeVisible (mousePosition);
}
private void StartSelecting ()

View File

@@ -222,10 +222,12 @@ public class RuneTests
[InlineData (
'\u4dc0',
"䷀",
1,
2,
1,
3
)] // ䷀Hexagram For The Creative Heaven - U+4dc0 - https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml
)] // ䷀Hexagram For The Creative Heaven - U+4dc0 - https://github.com/microsoft/terminal/blob/main/src/types/unicode_width_overrides.xml
// See https://github.com/microsoft/terminal/issues/19389
[InlineData ('\ud7b0', "ힰ", 1, 1, 3)] // ힰ ┤Hangul Jungseong O-Yeo - ힰ U+d7b0')]
[InlineData ('\uf61e', "", 1, 1, 3)] // Private Use Area
[InlineData ('\u23f0', "⏰", 2, 1, 3)] // Alarm Clock - ⏰ U+23f0