CharMap.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. #nullable enable
  2. using System.Diagnostics;
  3. using System.Diagnostics.CodeAnalysis;
  4. using System.Globalization;
  5. using System.Text.Json;
  6. namespace Terminal.Gui.Views;
  7. /// <summary>
  8. /// A scrollable map of the Unicode codepoints.
  9. /// </summary>
  10. /// <remarks>
  11. /// See <see href="../docs/CharacterMap.md"/> for details.
  12. /// </remarks>
  13. public class CharMap : View, IDesignable
  14. {
  15. private const int COLUMN_WIDTH = 3; // Width of each column of glyphs
  16. private const int HEADER_HEIGHT = 1; // Height of the header
  17. // ReSharper disable once InconsistentNaming
  18. private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
  19. /// <summary>
  20. /// Initializes a new instance.
  21. /// </summary>
  22. [RequiresUnreferencedCode ("AOT")]
  23. [RequiresDynamicCode ("AOT")]
  24. public CharMap ()
  25. {
  26. CanFocus = true;
  27. CursorVisibility = CursorVisibility.Default;
  28. AddCommand (Command.Up, commandContext => Move (commandContext, -16));
  29. AddCommand (Command.Down, commandContext => Move (commandContext, 16));
  30. AddCommand (Command.Left, commandContext => Move (commandContext, -1));
  31. AddCommand (Command.Right, commandContext => Move (commandContext, 1));
  32. AddCommand (Command.PageUp, commandContext => Move (commandContext, -(Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16));
  33. AddCommand (Command.PageDown, commandContext => Move (commandContext, (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16));
  34. AddCommand (Command.Start, commandContext => Move (commandContext, -SelectedCodePoint));
  35. AddCommand (Command.End, commandContext => Move (commandContext, MAX_CODE_POINT - SelectedCodePoint));
  36. AddCommand (Command.ScrollDown, () => ScrollVertical (1));
  37. AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
  38. AddCommand (Command.ScrollRight, () => ScrollHorizontal (1));
  39. AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1));
  40. AddCommand (Command.Accept, HandleAcceptCommand);
  41. AddCommand (Command.Select, HandleSelectCommand);
  42. AddCommand (Command.Context, HandleContextCommand);
  43. KeyBindings.Add (Key.CursorUp, Command.Up);
  44. KeyBindings.Add (Key.CursorDown, Command.Down);
  45. KeyBindings.Add (Key.CursorLeft, Command.Left);
  46. KeyBindings.Add (Key.CursorRight, Command.Right);
  47. KeyBindings.Add (Key.PageUp, Command.PageUp);
  48. KeyBindings.Add (Key.PageDown, Command.PageDown);
  49. KeyBindings.Add (Key.Home, Command.Start);
  50. KeyBindings.Add (Key.End, Command.End);
  51. KeyBindings.Add (PopoverMenu.DefaultKey, Command.Context);
  52. MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
  53. MouseBindings.ReplaceCommands (MouseFlags.Button3Clicked, Command.Context);
  54. MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Context);
  55. MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
  56. MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
  57. MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft);
  58. MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight);
  59. // Initial content size; height will be corrected by RebuildVisibleRows()
  60. SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, HEADER_HEIGHT + _rowHeight));
  61. // Set up the horizontal scrollbar. Turn off AutoShow since we do it manually.
  62. HorizontalScrollBar.AutoShow = false;
  63. HorizontalScrollBar.Increment = COLUMN_WIDTH;
  64. // This prevents scrolling past the last column
  65. HorizontalScrollBar.ScrollableContentSize = GetContentSize ().Width - RowLabelWidth;
  66. HorizontalScrollBar.X = RowLabelWidth;
  67. HorizontalScrollBar.Y = Pos.AnchorEnd ();
  68. HorizontalScrollBar.Width = Dim.Fill (1);
  69. // We want the horizontal scrollbar to only show when needed.
  70. // We can't use ScrollBar.AutoShow because we are using custom ContentSize
  71. // So, we do it manually on ViewportChanged events.
  72. ViewportChanged += (sender, args) =>
  73. {
  74. if (Viewport.Width < GetContentSize ().Width)
  75. {
  76. HorizontalScrollBar.Visible = true;
  77. }
  78. else
  79. {
  80. HorizontalScrollBar.Visible = false;
  81. }
  82. };
  83. // Set up the vertical scrollbar. Turn off AutoShow since it's always visible.
  84. VerticalScrollBar.AutoShow = true;
  85. VerticalScrollBar.Visible = false;
  86. VerticalScrollBar.X = Pos.AnchorEnd ();
  87. VerticalScrollBar.Y = HEADER_HEIGHT; // Header
  88. // The scrollbars are in the Padding. VisualRole.Focus/Active are used to draw the
  89. // CharMap headers. Override Padding to force it to draw to match.
  90. Padding!.GettingAttributeForRole += PaddingOnGettingAttributeForRole;
  91. // Build initial visible rows (all rows with at least one valid codepoint)
  92. RebuildVisibleRows ();
  93. }
  94. // Visible rows management: each entry is the starting code point of a 16-wide row
  95. private readonly List<int> _visibleRowStarts = new ();
  96. private readonly Dictionary<int, int> _rowStartToVisibleIndex = new ();
  97. private void RebuildVisibleRows ()
  98. {
  99. _visibleRowStarts.Clear ();
  100. _rowStartToVisibleIndex.Clear ();
  101. int maxRow = MAX_CODE_POINT / 16;
  102. for (var row = 0; row <= maxRow; row++)
  103. {
  104. int start = row * 16;
  105. bool anyValid = false;
  106. bool anyVisible = false;
  107. for (var col = 0; col < 16; col++)
  108. {
  109. int cp = start + col;
  110. if (cp > RuneExtensions.MaxUnicodeCodePoint)
  111. {
  112. break;
  113. }
  114. if (!Rune.IsValid (cp))
  115. {
  116. continue;
  117. }
  118. anyValid = true;
  119. if (!ShowUnicodeCategory.HasValue)
  120. {
  121. // With no filter, a row is displayed if it has any valid codepoint
  122. anyVisible = true;
  123. break;
  124. }
  125. var rune = new Rune (cp);
  126. Span<char> utf16 = new char [2];
  127. rune.EncodeToUtf16 (utf16);
  128. UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]);
  129. if (cat == ShowUnicodeCategory.Value)
  130. {
  131. anyVisible = true;
  132. break;
  133. }
  134. }
  135. if (anyValid && (!ShowUnicodeCategory.HasValue ? anyValid : anyVisible))
  136. {
  137. _rowStartToVisibleIndex [start] = _visibleRowStarts.Count;
  138. _visibleRowStarts.Add (start);
  139. }
  140. }
  141. // Update content size to match visible rows
  142. SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, _visibleRowStarts.Count * _rowHeight + HEADER_HEIGHT));
  143. // Keep vertical scrollbar aligned with new content size
  144. VerticalScrollBar.ScrollableContentSize = GetContentSize ().Height;
  145. }
  146. private int VisibleRowIndexForCodePoint (int codePoint)
  147. {
  148. int start = (codePoint / 16) * 16;
  149. return _rowStartToVisibleIndex.GetValueOrDefault (start, -1);
  150. }
  151. private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested
  152. private int _selectedCodepoint; // Currently selected codepoint
  153. private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport
  154. /// <summary>
  155. /// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
  156. /// visible.
  157. /// </summary>
  158. public int SelectedCodePoint
  159. {
  160. get => _selectedCodepoint;
  161. set
  162. {
  163. if (_selectedCodepoint == value)
  164. {
  165. return;
  166. }
  167. int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT);
  168. Point offsetToNewCursor = GetCursor (newSelectedCodePoint);
  169. _selectedCodepoint = newSelectedCodePoint;
  170. // Ensure the new cursor position is visible
  171. ScrollToMakeCursorVisible (offsetToNewCursor);
  172. SetNeedsDraw ();
  173. SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint));
  174. }
  175. }
  176. /// <summary>
  177. /// Raised when the selected code point changes.
  178. /// </summary>
  179. public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
  180. /// <summary>
  181. /// Gets or sets whether the number of columns each glyph is displayed.
  182. /// </summary>
  183. public bool ShowGlyphWidths
  184. {
  185. get => _rowHeight == 2;
  186. set
  187. {
  188. _rowHeight = value ? 2 : 1;
  189. // height changed => content height depends on row height
  190. RebuildVisibleRows ();
  191. SetNeedsDraw ();
  192. }
  193. }
  194. /// <summary>
  195. /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
  196. /// characters.
  197. /// </summary>
  198. public int StartCodePoint
  199. {
  200. get => _startCodepoint;
  201. set
  202. {
  203. _startCodepoint = value;
  204. SelectedCodePoint = value;
  205. }
  206. }
  207. private UnicodeCategory? _showUnicodeCategory;
  208. /// <summary>
  209. /// When set, only glyphs whose UnicodeCategory matches the value are rendered. If <see langword="null"/> (default),
  210. /// all glyphs are rendered.
  211. /// </summary>
  212. public UnicodeCategory? ShowUnicodeCategory
  213. {
  214. get => _showUnicodeCategory;
  215. set
  216. {
  217. if (_showUnicodeCategory == value)
  218. {
  219. return;
  220. }
  221. _showUnicodeCategory = value;
  222. RebuildVisibleRows ();
  223. // Ensure selection is on a visible row
  224. int desiredRowStart = (SelectedCodePoint / 16) * 16;
  225. if (!_rowStartToVisibleIndex.ContainsKey (desiredRowStart))
  226. {
  227. // Find nearest visible row (prefer next; fallback to last)
  228. int idx = _visibleRowStarts.FindIndex (s => s >= desiredRowStart);
  229. if (idx < 0 && _visibleRowStarts.Count > 0)
  230. {
  231. idx = _visibleRowStarts.Count - 1;
  232. }
  233. if (idx >= 0)
  234. {
  235. SelectedCodePoint = _visibleRowStarts [idx];
  236. }
  237. }
  238. SetNeedsDraw ();
  239. }
  240. }
  241. private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
  242. private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
  243. private bool? Move (ICommandContext? commandContext, int cpOffset)
  244. {
  245. if (RaiseSelecting (commandContext) is true)
  246. {
  247. return true;
  248. }
  249. SelectedCodePoint += cpOffset;
  250. return true;
  251. }
  252. private void PaddingOnGettingAttributeForRole (object? sender, VisualRoleEventArgs e)
  253. {
  254. if (e.Role != VisualRole.Focus && e.Role != VisualRole.Active)
  255. {
  256. e.Result = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
  257. }
  258. e.Handled = true;
  259. }
  260. private void ScrollToMakeCursorVisible (Point offsetToNewCursor)
  261. {
  262. // Adjust vertical scrolling
  263. if (offsetToNewCursor.Y < 1) // Header is at Y = 0
  264. {
  265. ScrollVertical (offsetToNewCursor.Y - HEADER_HEIGHT);
  266. }
  267. else if (offsetToNewCursor.Y >= Viewport.Height)
  268. {
  269. ScrollVertical (offsetToNewCursor.Y - Viewport.Height + HEADER_HEIGHT);
  270. }
  271. // Adjust horizontal scrolling
  272. if (offsetToNewCursor.X < RowLabelWidth + 1)
  273. {
  274. ScrollHorizontal (offsetToNewCursor.X - (RowLabelWidth + 1));
  275. }
  276. else if (offsetToNewCursor.X >= Viewport.Width)
  277. {
  278. ScrollHorizontal (offsetToNewCursor.X - Viewport.Width + 1);
  279. }
  280. }
  281. #region Details Dialog
  282. [RequiresUnreferencedCode ("AOT")]
  283. [RequiresDynamicCode ("AOT")]
  284. private void ShowDetails ()
  285. {
  286. if (!Application.Initialized)
  287. {
  288. // Some unit tests invoke Accept without Init
  289. return;
  290. }
  291. UcdApiClient? client = new ();
  292. var decResponse = string.Empty;
  293. var getCodePointError = string.Empty;
  294. Dialog? waitIndicator = new ()
  295. {
  296. Title = Strings.charMapCPInfoDlgTitle,
  297. X = Pos.Center (),
  298. Y = Pos.Center (),
  299. Width = 40,
  300. Height = 10,
  301. Buttons = [new () { Text = Strings.btnCancel }]
  302. };
  303. var errorLabel = new Label
  304. {
  305. Text = UcdApiClient.BaseUrl,
  306. X = 0,
  307. Y = 0,
  308. Width = Dim.Fill (),
  309. Height = Dim.Fill (3),
  310. TextAlignment = Alignment.Center
  311. };
  312. var spinner = new SpinnerView
  313. {
  314. X = Pos.Center (),
  315. Y = Pos.Bottom (errorLabel),
  316. Style = new SpinnerStyle.Aesthetic ()
  317. };
  318. spinner.AutoSpin = true;
  319. waitIndicator.Add (errorLabel);
  320. waitIndicator.Add (spinner);
  321. waitIndicator.Ready += async (s, a) =>
  322. {
  323. try
  324. {
  325. decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false);
  326. Application.Invoke (() => waitIndicator.RequestStop ());
  327. }
  328. catch (HttpRequestException e)
  329. {
  330. getCodePointError = errorLabel.Text = e.Message;
  331. Application.Invoke (() => waitIndicator.RequestStop ());
  332. }
  333. };
  334. Application.Run (waitIndicator);
  335. waitIndicator.Dispose ();
  336. var name = string.Empty;
  337. if (!string.IsNullOrEmpty (decResponse))
  338. {
  339. using JsonDocument document = JsonDocument.Parse (decResponse);
  340. JsonElement root = document.RootElement;
  341. // Get a property by name and output its value
  342. if (root.TryGetProperty ("name", out JsonElement nameElement))
  343. {
  344. name = nameElement.GetString ();
  345. }
  346. decResponse = JsonSerializer.Serialize (
  347. document.RootElement,
  348. new
  349. JsonSerializerOptions
  350. { WriteIndented = true }
  351. );
  352. }
  353. else
  354. {
  355. decResponse = getCodePointError;
  356. }
  357. var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
  358. Button? copyGlyph = new () { Text = Strings.charMapCopyGlyph };
  359. Button? copyCodepoint = new () { Text = Strings.charMapCopyCP };
  360. Button? cancel = new () { Text = Strings.btnCancel };
  361. var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
  362. copyGlyph.Accepting += (s, a) =>
  363. {
  364. CopyGlyph ();
  365. dlg!.RequestStop ();
  366. a.Handled = true;
  367. };
  368. copyCodepoint.Accepting += (s, a) =>
  369. {
  370. CopyCodePoint ();
  371. dlg!.RequestStop ();
  372. a.Handled = true;
  373. };
  374. cancel.Accepting += (s, a) =>
  375. {
  376. dlg!.RequestStop ();
  377. a.Handled = true;
  378. };
  379. var rune = (Rune)SelectedCodePoint;
  380. var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
  381. dlg.Add (label);
  382. label = new () { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
  383. dlg.Add (label);
  384. label = new () { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
  385. dlg.Add (label);
  386. label = new () { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
  387. dlg.Add (label);
  388. label = new () { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
  389. dlg.Add (label);
  390. label = new () { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
  391. dlg.Add (label);
  392. label = new () { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
  393. dlg.Add (label);
  394. label = new () { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
  395. dlg.Add (label);
  396. label = new () { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
  397. dlg.Add (label);
  398. label = new () { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
  399. dlg.Add (label);
  400. label = new () { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
  401. dlg.Add (label);
  402. label = new () { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
  403. dlg.Add (label);
  404. label = new () { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
  405. dlg.Add (label);
  406. label = new () { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
  407. dlg.Add (label);
  408. label = new () { Text = "Category: ", X = 0, Y = Pos.Bottom (label) };
  409. dlg.Add (label);
  410. Span<char> utf16 = stackalloc char [2];
  411. int charCount = rune.EncodeToUtf16 (utf16);
  412. // Get the bidi class for the first code unit
  413. // For most bidi characters, the first code unit is sufficient
  414. UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]);
  415. label = new () { Text = $"{category}", X = Pos.Right (label), Y = Pos.Top (label) };
  416. dlg.Add (label);
  417. label = new ()
  418. {
  419. Text =
  420. $"{Strings.charMapInfoDlgInfoLabel} {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
  421. X = 0,
  422. Y = Pos.Bottom (label)
  423. };
  424. dlg.Add (label);
  425. var json = new TextView
  426. {
  427. X = 0,
  428. Y = Pos.Bottom (label),
  429. Width = Dim.Fill (),
  430. Height = Dim.Fill (2),
  431. ReadOnly = true,
  432. Text = decResponse
  433. };
  434. dlg.Add (json);
  435. Application.Run (dlg);
  436. dlg.Dispose ();
  437. }
  438. #endregion Details Dialog
  439. #region Cursor
  440. private Point GetCursor (int codePoint)
  441. {
  442. // + 1 for padding between label and first column
  443. int x = codePoint % 16 * COLUMN_WIDTH + RowLabelWidth + 1 - Viewport.X;
  444. int visibleRowIndex = VisibleRowIndexForCodePoint (codePoint);
  445. if (visibleRowIndex < 0)
  446. {
  447. // If filtered out, stick to current Y to avoid jumping; caller will clamp
  448. int fallbackY = HEADER_HEIGHT - Viewport.Y;
  449. return new (x, fallbackY);
  450. }
  451. int y = visibleRowIndex * _rowHeight + HEADER_HEIGHT - Viewport.Y;
  452. return new (x, y);
  453. }
  454. /// <inheritdoc/>
  455. public override Point? PositionCursor ()
  456. {
  457. Point cursor = GetCursor (SelectedCodePoint);
  458. if (HasFocus
  459. && cursor.X >= RowLabelWidth
  460. && cursor.X < Viewport.Width
  461. && cursor.Y > 0
  462. && cursor.Y < Viewport.Height)
  463. {
  464. Move (cursor.X, cursor.Y);
  465. }
  466. else
  467. {
  468. return null;
  469. }
  470. return cursor;
  471. }
  472. #endregion Cursor
  473. #region Drawing
  474. private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
  475. /// <inheritdoc/>
  476. protected override bool OnDrawingContent ()
  477. {
  478. if (Viewport.Height == 0 || Viewport.Width == 0)
  479. {
  480. return true;
  481. }
  482. int selectedCol = SelectedCodePoint % 16;
  483. int selectedRowIndex = VisibleRowIndexForCodePoint (SelectedCodePoint);
  484. // Headers
  485. // Clear the header area
  486. Move (0, 0);
  487. SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
  488. AddStr (new (' ', Viewport.Width));
  489. int firstColumnX = RowLabelWidth - Viewport.X;
  490. // Header
  491. var x = 0;
  492. for (var hexDigit = 0; hexDigit < 16; hexDigit++)
  493. {
  494. x = firstColumnX + hexDigit * COLUMN_WIDTH;
  495. if (x > RowLabelWidth - 2)
  496. {
  497. Move (x, 0);
  498. SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
  499. AddStr (" ");
  500. // Swap Active/Focus so the selected column is highlighted
  501. if (hexDigit == selectedCol)
  502. {
  503. SetAttributeForRole (HasFocus ? VisualRole.Active : VisualRole.Focus);
  504. }
  505. AddStr ($"{hexDigit:x}");
  506. SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
  507. AddStr (" ");
  508. }
  509. }
  510. // Start at 1 because Header.
  511. for (var y = 1; y < Viewport.Height; y++)
  512. {
  513. // Which visible row is this?
  514. int visibleRow = (y + Viewport.Y - 1) / _rowHeight;
  515. if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count)
  516. {
  517. // No row at this y; clear label area and continue
  518. Move (0, y);
  519. AddStr (new (' ', Viewport.Width));
  520. continue;
  521. }
  522. int rowStart = _visibleRowStarts [visibleRow];
  523. // Draw the row label (U+XXXX_)
  524. SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active);
  525. Move (0, y);
  526. // Swap Active/Focus so the selected row is highlighted
  527. if (visibleRow == selectedRowIndex)
  528. {
  529. SetAttributeForRole (HasFocus ? VisualRole.Active : VisualRole.Focus);
  530. }
  531. if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
  532. {
  533. AddStr ($"U+{rowStart / 16:x5}_");
  534. }
  535. else
  536. {
  537. AddStr (new (' ', RowLabelWidth));
  538. }
  539. // Draw the row
  540. SetAttributeForRole (VisualRole.Normal);
  541. for (var col = 0; col < 16; col++)
  542. {
  543. x = firstColumnX + COLUMN_WIDTH * col + 1;
  544. if (x < RowLabelWidth || x > Viewport.Width - 1)
  545. {
  546. continue;
  547. }
  548. Move (x, y);
  549. // If we're at the cursor position highlight the cell
  550. if (visibleRow == selectedRowIndex && col == selectedCol)
  551. {
  552. SetAttributeForRole (VisualRole.Active);
  553. }
  554. int scalar = rowStart + col;
  555. // Don't render out-of-range scalars
  556. if (scalar > MAX_CODE_POINT)
  557. {
  558. AddRune (' ');
  559. if (visibleRow == selectedRowIndex && col == selectedCol)
  560. {
  561. SetAttributeForRole (VisualRole.Normal);
  562. }
  563. continue;
  564. }
  565. var rune = (Rune)'?';
  566. if (Rune.IsValid (scalar))
  567. {
  568. rune = new (scalar);
  569. }
  570. int width = rune.GetColumns ();
  571. // Compute visibility based on ShowUnicodeCategory
  572. bool isVisible = Rune.IsValid (scalar);
  573. if (isVisible && ShowUnicodeCategory.HasValue)
  574. {
  575. Span<char> filterUtf16 = new char [2];
  576. rune.EncodeToUtf16 (filterUtf16);
  577. UnicodeCategory cat = CharUnicodeInfo.GetUnicodeCategory (filterUtf16 [0]);
  578. isVisible = cat == ShowUnicodeCategory.Value;
  579. }
  580. if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
  581. {
  582. // Glyph row
  583. if (isVisible)
  584. {
  585. RenderRune (rune, width);
  586. }
  587. else
  588. {
  589. AddRune (' ');
  590. }
  591. }
  592. else
  593. {
  594. // Width row (ShowGlyphWidths)
  595. if (isVisible)
  596. {
  597. // Draw the width of the rune faint
  598. Attribute attr = GetAttributeForRole (VisualRole.Normal);
  599. SetAttribute (attr with { Style = attr.Style | TextStyle.Faint });
  600. AddStr ($"{width}");
  601. }
  602. else
  603. {
  604. AddRune (' ');
  605. }
  606. }
  607. // If we're at the cursor position, and we don't have focus
  608. if (visibleRow == selectedRowIndex && col == selectedCol)
  609. {
  610. SetAttributeForRole (VisualRole.Normal);
  611. }
  612. }
  613. }
  614. return true;
  615. void RenderRune (Rune rune, int width)
  616. {
  617. // Get the UnicodeCategory
  618. Span<char> utf16 = new char [2];
  619. int charCount = rune.EncodeToUtf16 (utf16);
  620. // Get the bidi class for the first code unit
  621. // For most bidi characters, the first code unit is sufficient
  622. UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory (utf16 [0]);
  623. switch (category)
  624. {
  625. case UnicodeCategory.OtherNotAssigned:
  626. SetAttributeForRole (VisualRole.Highlight);
  627. AddRune (Rune.ReplacementChar);
  628. SetAttributeForRole (VisualRole.Normal);
  629. break;
  630. // Format character that affects the layout of text or the operation of text processes, but is not normally rendered.
  631. // These report width of 0 and don't render on their own.
  632. case UnicodeCategory.Format:
  633. SetAttributeForRole (VisualRole.Highlight);
  634. AddRune ('F');
  635. SetAttributeForRole (VisualRole.Normal);
  636. break;
  637. // Nonspacing character that indicates modifications of a base character.
  638. case UnicodeCategory.NonSpacingMark:
  639. // Spacing character that indicates modifications of a base character and affects the width of the glyph for that base character.
  640. case UnicodeCategory.SpacingCombiningMark:
  641. // Enclosing mark character, which is a nonspacing combining character that surrounds all previous characters up to and including a base character.
  642. case UnicodeCategory.EnclosingMark:
  643. if (width > 0)
  644. {
  645. AddRune (rune);
  646. }
  647. else
  648. {
  649. if (rune.IsCombiningMark ())
  650. {
  651. // This is a hack to work around the fact that combining marks
  652. // a) can't be rendered on their own
  653. // b) that don't normalize are not properly supported in
  654. // any known terminal (esp Windows/AtlasEngine).
  655. // See Issue #2616
  656. var sb = new StringBuilder ();
  657. sb.Append ('a');
  658. sb.Append (rune);
  659. // Try normalizing after combining with 'a'. If it normalizes, at least
  660. // it'll show on the 'a'. If not, just show the replacement char.
  661. string normal = sb.ToString ().Normalize (NormalizationForm.FormC);
  662. if (normal.Length == 1)
  663. {
  664. AddRune ((Rune)normal [0]);
  665. }
  666. else
  667. {
  668. SetAttributeForRole (VisualRole.Highlight);
  669. AddRune ('M');
  670. SetAttributeForRole (VisualRole.Normal);
  671. }
  672. }
  673. }
  674. break;
  675. // These report width of 0, but render as 1
  676. case UnicodeCategory.Control:
  677. case UnicodeCategory.LineSeparator:
  678. case UnicodeCategory.ParagraphSeparator:
  679. case UnicodeCategory.Surrogate:
  680. AddRune (rune);
  681. break;
  682. default:
  683. // Draw the rune
  684. if (width > 0)
  685. {
  686. AddRune (rune);
  687. }
  688. else
  689. {
  690. throw new InvalidOperationException ($"The Rune \"{rune}\" (U+{rune.Value:x6}) has zero width and no special-case UnicodeCategory logic applies.");
  691. }
  692. break;
  693. }
  694. }
  695. }
  696. /// <summary>
  697. /// Helper to convert a string into camel case.
  698. /// </summary>
  699. /// <param name="str"></param>
  700. /// <returns></returns>
  701. public static string ToCamelCase (string str)
  702. {
  703. if (string.IsNullOrEmpty (str))
  704. {
  705. return str;
  706. }
  707. TextInfo textInfo = new CultureInfo ("en-US", false).TextInfo;
  708. str = textInfo.ToLower (str);
  709. str = textInfo.ToTitleCase (str);
  710. return str;
  711. }
  712. #endregion Drawing
  713. #region Mouse Handling
  714. private bool? HandleSelectCommand (ICommandContext? commandContext)
  715. {
  716. Point position = GetCursor (SelectedCodePoint);
  717. if (commandContext is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
  718. {
  719. // If the mouse is clicked on the headers, map it to the first glyph of the row/col
  720. position = mouseCommandContext.Binding.MouseEventArgs.Position;
  721. if (position.Y == 0)
  722. {
  723. position = position with { Y = GetCursor (SelectedCodePoint).Y };
  724. }
  725. if (position.X < RowLabelWidth || position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
  726. {
  727. position = position with { X = GetCursor (SelectedCodePoint).X };
  728. }
  729. }
  730. if (RaiseSelecting (commandContext) is true)
  731. {
  732. return true;
  733. }
  734. if (!TryGetCodePointFromPosition (position, out int cp))
  735. {
  736. return false;
  737. }
  738. if (cp != SelectedCodePoint)
  739. {
  740. if (!HasFocus && CanFocus)
  741. {
  742. SetFocus ();
  743. }
  744. SelectedCodePoint = cp;
  745. }
  746. return true;
  747. }
  748. [RequiresUnreferencedCode ("AOT")]
  749. [RequiresDynamicCode ("AOT")]
  750. private bool? HandleAcceptCommand (ICommandContext? commandContext)
  751. {
  752. if (RaiseAccepting (commandContext) is true)
  753. {
  754. return true;
  755. }
  756. if (commandContext is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
  757. {
  758. if (!HasFocus && CanFocus)
  759. {
  760. SetFocus ();
  761. }
  762. if (!TryGetCodePointFromPosition (mouseCommandContext.Binding.MouseEventArgs.Position, out int cp))
  763. {
  764. return false;
  765. }
  766. SelectedCodePoint = cp;
  767. }
  768. ShowDetails ();
  769. return true;
  770. }
  771. private bool? HandleContextCommand (ICommandContext? commandContext)
  772. {
  773. int newCodePoint = SelectedCodePoint;
  774. if (commandContext is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } } mouseCommandContext)
  775. {
  776. if (!TryGetCodePointFromPosition (mouseCommandContext.Binding.MouseEventArgs.Position, out newCodePoint))
  777. {
  778. return false;
  779. }
  780. }
  781. if (!HasFocus && CanFocus)
  782. {
  783. SetFocus ();
  784. }
  785. SelectedCodePoint = newCodePoint;
  786. // This demonstrates how to create an ephemeral Popover; one that exists
  787. // ony as long as the popover is visible.
  788. // Note, for ephemeral Popovers, hotkeys are not supported.
  789. PopoverMenu? contextMenu = new (
  790. [
  791. new (Strings.charMapCopyGlyph, string.Empty, CopyGlyph),
  792. new (Strings.charMapCopyCP, string.Empty, CopyCodePoint)
  793. ]);
  794. // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
  795. // and the context menu is disposed when it is closed.
  796. Application.Popover?.Register (contextMenu);
  797. contextMenu?.MakeVisible (ViewportToScreen (GetCursor (SelectedCodePoint)));
  798. return true;
  799. }
  800. private bool TryGetCodePointFromPosition (Point position, out int codePoint)
  801. {
  802. if (position.X < RowLabelWidth || position.Y < 1)
  803. {
  804. codePoint = 0;
  805. return false;
  806. }
  807. int visibleRow = (position.Y - 1 - -Viewport.Y) / _rowHeight;
  808. if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count)
  809. {
  810. codePoint = 0;
  811. return false;
  812. }
  813. int col = (position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
  814. if (col > 15)
  815. {
  816. col = 15;
  817. }
  818. codePoint = _visibleRowStarts [visibleRow] + col;
  819. if (codePoint > MAX_CODE_POINT)
  820. {
  821. return false;
  822. }
  823. return true;
  824. }
  825. #endregion Mouse Handling
  826. }