CharacterMap.cs 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. #define OTHER_CONTROLS
  2. using System;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Net.Http;
  8. using System.Reflection;
  9. using System.Text;
  10. using System.Text.Json;
  11. using System.Text.Unicode;
  12. using System.Threading.Tasks;
  13. using Terminal.Gui;
  14. using static Terminal.Gui.SpinnerStyle;
  15. namespace UICatalog.Scenarios;
  16. /// <summary>
  17. /// This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
  18. /// "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
  19. /// Illustrates how to do infinite scrolling
  20. /// </summary>
  21. [ScenarioMetadata ("Character Map", "Unicode viewer demonstrating infinite content, scrolling, and Unicode.")]
  22. [ScenarioCategory ("Text and Formatting")]
  23. [ScenarioCategory ("Drawing")]
  24. [ScenarioCategory ("Controls")]
  25. [ScenarioCategory ("Layout")]
  26. [ScenarioCategory ("Scrolling")]
  27. public class CharacterMap : Scenario
  28. {
  29. public Label _errorLabel;
  30. private TableView _categoryList;
  31. private CharMap _charMap;
  32. // Don't create a Window, just return the top-level view
  33. public override void Main ()
  34. {
  35. Application.Init ();
  36. var top = new Window
  37. {
  38. BorderStyle = LineStyle.None
  39. };
  40. _charMap = new ()
  41. {
  42. X = 0,
  43. Y = 0,
  44. Width = Dim.Fill (),
  45. Height = Dim.Fill ()
  46. };
  47. top.Add (_charMap);
  48. #if OTHER_CONTROLS
  49. _charMap.Y = 1;
  50. var jumpLabel = new Label
  51. {
  52. X = Pos.Right (_charMap) + 1,
  53. Y = Pos.Y (_charMap),
  54. HotKeySpecifier = (Rune)'_',
  55. Text = "_Jump To Code Point:"
  56. };
  57. top.Add (jumpLabel);
  58. var jumpEdit = new TextField
  59. {
  60. X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3"
  61. };
  62. top.Add (jumpEdit);
  63. _errorLabel = new ()
  64. {
  65. X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"], Text = "err"
  66. };
  67. top.Add (_errorLabel);
  68. jumpEdit.Accepting += JumpEditOnAccept;
  69. _categoryList = new () { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () };
  70. _categoryList.FullRowSelect = true;
  71. //jumpList.Style.ShowHeaders = false;
  72. //jumpList.Style.ShowHorizontalHeaderOverline = false;
  73. //jumpList.Style.ShowHorizontalHeaderUnderline = false;
  74. _categoryList.Style.ShowHorizontalBottomline = true;
  75. //jumpList.Style.ShowVerticalCellLines = false;
  76. //jumpList.Style.ShowVerticalHeaderLines = false;
  77. _categoryList.Style.AlwaysShowHeaders = true;
  78. var isDescending = false;
  79. _categoryList.Table = CreateCategoryTable (0, isDescending);
  80. // if user clicks the mouse in TableView
  81. _categoryList.MouseClick += (s, e) =>
  82. {
  83. _categoryList.ScreenToCell (e.MouseEvent.Position, out int? clickedCol);
  84. if (clickedCol != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
  85. {
  86. EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  87. string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
  88. isDescending = !isDescending;
  89. _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
  90. table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  91. _categoryList.SelectedRow = table.Data
  92. .Select ((item, index) => new { item, index })
  93. .FirstOrDefault (x => x.item.Category == prevSelection)
  94. ?.index
  95. ?? -1;
  96. }
  97. };
  98. int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
  99. _categoryList.Style.ColumnStyles.Add (
  100. 0,
  101. new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
  102. );
  103. _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
  104. _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
  105. _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
  106. _categoryList.SelectedCellChanged += (s, args) =>
  107. {
  108. EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  109. _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
  110. };
  111. top.Add (_categoryList);
  112. // TODO: Replace this with Dim.Auto when that's ready
  113. _categoryList.Initialized += _categoryList_Initialized;
  114. var menu = new MenuBar
  115. {
  116. Menus =
  117. [
  118. new (
  119. "_File",
  120. new MenuItem []
  121. {
  122. new (
  123. "_Quit",
  124. $"{Application.QuitKey}",
  125. () => Application.RequestStop ()
  126. )
  127. }
  128. ),
  129. new (
  130. "_Options",
  131. new [] { CreateMenuShowWidth () }
  132. )
  133. ]
  134. };
  135. top.Add (menu);
  136. #endif // OTHER_CONTROLS
  137. _charMap.SelectedCodePoint = 0;
  138. _charMap.SetFocus ();
  139. Application.Run (top);
  140. top.Dispose ();
  141. Application.Shutdown ();
  142. return;
  143. void JumpEditOnAccept (object sender, CommandEventArgs e)
  144. {
  145. if (jumpEdit.Text.Length == 0)
  146. {
  147. return;
  148. }
  149. uint result = 0;
  150. if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
  151. {
  152. try
  153. {
  154. result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
  155. }
  156. catch (FormatException)
  157. {
  158. _errorLabel.Text = "Invalid hex value";
  159. return;
  160. }
  161. }
  162. else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
  163. {
  164. try
  165. {
  166. result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
  167. }
  168. catch (FormatException)
  169. {
  170. _errorLabel.Text = "Invalid hex value";
  171. return;
  172. }
  173. }
  174. else
  175. {
  176. try
  177. {
  178. result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
  179. }
  180. catch (FormatException)
  181. {
  182. _errorLabel.Text = "Invalid value";
  183. return;
  184. }
  185. }
  186. if (result > RuneExtensions.MaxUnicodeCodePoint)
  187. {
  188. _errorLabel.Text = "Beyond maximum codepoint";
  189. return;
  190. }
  191. _errorLabel.Text = $"U+{result:x5}";
  192. EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  193. _categoryList.SelectedRow = table.Data
  194. .Select ((item, index) => new { item, index })
  195. .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
  196. ?.index
  197. ?? -1;
  198. _categoryList.EnsureSelectedCellIsVisible ();
  199. // Ensure the typed glyph is selected
  200. _charMap.SelectedCodePoint = (int)result;
  201. // Cancel the event to prevent ENTER from being handled elsewhere
  202. e.Cancel = true;
  203. }
  204. }
  205. private void _categoryList_Initialized (object sender, EventArgs e) { _charMap.Width = Dim.Fill () - _categoryList.Width; }
  206. private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
  207. {
  208. Func<UnicodeRange, object> orderBy;
  209. var categorySort = string.Empty;
  210. var startSort = string.Empty;
  211. var endSort = string.Empty;
  212. string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString ();
  213. switch (sortByColumn)
  214. {
  215. case 0:
  216. orderBy = r => r.Category;
  217. categorySort = sortIndicator;
  218. break;
  219. case 1:
  220. orderBy = r => r.Start;
  221. startSort = sortIndicator;
  222. break;
  223. case 2:
  224. orderBy = r => r.End;
  225. endSort = sortIndicator;
  226. break;
  227. default:
  228. throw new ArgumentException ("Invalid column number.");
  229. }
  230. IOrderedEnumerable<UnicodeRange> sortedRanges = descending
  231. ? UnicodeRange.Ranges.OrderByDescending (orderBy)
  232. : UnicodeRange.Ranges.OrderBy (orderBy);
  233. return new (
  234. sortedRanges,
  235. new ()
  236. {
  237. { $"Category{categorySort}", s => s.Category },
  238. { $"Start{startSort}", s => $"{s.Start:x5}" },
  239. { $"End{endSort}", s => $"{s.End:x5}" }
  240. }
  241. );
  242. }
  243. private MenuItem CreateMenuShowWidth ()
  244. {
  245. var item = new MenuItem { Title = "_Show Glyph Width" };
  246. item.CheckType |= MenuItemCheckStyle.Checked;
  247. item.Checked = _charMap?.ShowGlyphWidths;
  248. item.Action += () => { _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked); };
  249. return item;
  250. }
  251. }
  252. internal class CharMap : View
  253. {
  254. private const int COLUMN_WIDTH = 3;
  255. private ContextMenu _contextMenu = new ();
  256. private int _rowHeight = 1;
  257. private int _selected;
  258. private int _start;
  259. public CharMap ()
  260. {
  261. ColorScheme = Colors.ColorSchemes ["Dialog"];
  262. CanFocus = true;
  263. CursorVisibility = CursorVisibility.Default;
  264. SetContentSize (new (RowWidth, (MaxCodePoint / 16 + 2) * _rowHeight));
  265. AddCommand (
  266. Command.ScrollUp,
  267. () =>
  268. {
  269. if (SelectedCodePoint >= 16)
  270. {
  271. SelectedCodePoint -= 16;
  272. }
  273. ScrollVertical (-_rowHeight);
  274. return true;
  275. }
  276. );
  277. AddCommand (
  278. Command.ScrollDown,
  279. () =>
  280. {
  281. if (SelectedCodePoint <= MaxCodePoint - 16)
  282. {
  283. SelectedCodePoint += 16;
  284. }
  285. if (Cursor.Y >= Viewport.Height)
  286. {
  287. ScrollVertical (_rowHeight);
  288. }
  289. return true;
  290. }
  291. );
  292. AddCommand (
  293. Command.ScrollLeft,
  294. () =>
  295. {
  296. if (SelectedCodePoint > 0)
  297. {
  298. SelectedCodePoint--;
  299. }
  300. if (Cursor.X > RowLabelWidth + 1)
  301. {
  302. ScrollHorizontal (-COLUMN_WIDTH);
  303. }
  304. return true;
  305. }
  306. );
  307. AddCommand (
  308. Command.ScrollRight,
  309. () =>
  310. {
  311. if (SelectedCodePoint < MaxCodePoint)
  312. {
  313. SelectedCodePoint++;
  314. }
  315. if (Cursor.X >= Viewport.Width)
  316. {
  317. ScrollHorizontal (COLUMN_WIDTH);
  318. }
  319. return true;
  320. }
  321. );
  322. AddCommand (
  323. Command.PageUp,
  324. () =>
  325. {
  326. int page = (Viewport.Height - 1 / _rowHeight) * 16;
  327. SelectedCodePoint -= Math.Min (page, SelectedCodePoint);
  328. Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
  329. return true;
  330. }
  331. );
  332. AddCommand (
  333. Command.PageDown,
  334. () =>
  335. {
  336. int page = (Viewport.Height - 1 / _rowHeight) * 16;
  337. SelectedCodePoint += Math.Min (page, MaxCodePoint - SelectedCodePoint);
  338. Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
  339. return true;
  340. }
  341. );
  342. AddCommand (
  343. Command.Start,
  344. () =>
  345. {
  346. SelectedCodePoint = 0;
  347. return true;
  348. }
  349. );
  350. AddCommand (
  351. Command.End,
  352. () =>
  353. {
  354. SelectedCodePoint = MaxCodePoint;
  355. Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
  356. return true;
  357. }
  358. );
  359. AddCommand (
  360. Command.Accept,
  361. () =>
  362. {
  363. ShowDetails ();
  364. return true;
  365. }
  366. );
  367. KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
  368. KeyBindings.Add (Key.CursorDown, Command.ScrollDown);
  369. KeyBindings.Add (Key.CursorLeft, Command.ScrollLeft);
  370. KeyBindings.Add (Key.CursorRight, Command.ScrollRight);
  371. KeyBindings.Add (Key.PageUp, Command.PageUp);
  372. KeyBindings.Add (Key.PageDown, Command.PageDown);
  373. KeyBindings.Add (Key.Home, Command.Start);
  374. KeyBindings.Add (Key.End, Command.End);
  375. MouseClick += Handle_MouseClick;
  376. MouseEvent += Handle_MouseEvent;
  377. // Prototype scrollbars
  378. Padding.Thickness = new (0, 0, 1, 1);
  379. var up = new Button
  380. {
  381. X = Pos.AnchorEnd (1),
  382. Y = 0,
  383. Height = 1,
  384. Width = 1,
  385. NoPadding = true,
  386. NoDecorations = true,
  387. Title = CM.Glyphs.UpArrow.ToString (),
  388. WantContinuousButtonPressed = true,
  389. ShadowStyle = ShadowStyle.None,
  390. CanFocus = false
  391. };
  392. up.Accepting += (sender, args) => { args.Cancel = ScrollVertical (-1) == true; };
  393. var down = new Button
  394. {
  395. X = Pos.AnchorEnd (1),
  396. Y = Pos.AnchorEnd (2),
  397. Height = 1,
  398. Width = 1,
  399. NoPadding = true,
  400. NoDecorations = true,
  401. Title = CM.Glyphs.DownArrow.ToString (),
  402. WantContinuousButtonPressed = true,
  403. ShadowStyle = ShadowStyle.None,
  404. CanFocus = false
  405. };
  406. down.Accepting += (sender, args) => { ScrollVertical (1); };
  407. var left = new Button
  408. {
  409. X = 0,
  410. Y = Pos.AnchorEnd (1),
  411. Height = 1,
  412. Width = 1,
  413. NoPadding = true,
  414. NoDecorations = true,
  415. Title = CM.Glyphs.LeftArrow.ToString (),
  416. WantContinuousButtonPressed = true,
  417. ShadowStyle = ShadowStyle.None,
  418. CanFocus = false
  419. };
  420. left.Accepting += (sender, args) => { ScrollHorizontal (-1); };
  421. var right = new Button
  422. {
  423. X = Pos.AnchorEnd (2),
  424. Y = Pos.AnchorEnd (1),
  425. Height = 1,
  426. Width = 1,
  427. NoPadding = true,
  428. NoDecorations = true,
  429. Title = CM.Glyphs.RightArrow.ToString (),
  430. WantContinuousButtonPressed = true,
  431. ShadowStyle = ShadowStyle.None,
  432. CanFocus = false
  433. };
  434. right.Accepting += (sender, args) => { ScrollHorizontal (1); };
  435. Padding.Add (up, down, left, right);
  436. }
  437. private void Handle_MouseEvent (object sender, MouseEventArgs e)
  438. {
  439. if (e.MouseEvent.Flags == MouseFlags.WheeledDown)
  440. {
  441. ScrollVertical (1);
  442. e.Handled = true;
  443. return;
  444. }
  445. if (e.MouseEvent.Flags == MouseFlags.WheeledUp)
  446. {
  447. ScrollVertical (-1);
  448. e.Handled = true;
  449. return;
  450. }
  451. if (e.MouseEvent.Flags == MouseFlags.WheeledRight)
  452. {
  453. ScrollHorizontal (1);
  454. e.Handled = true;
  455. return;
  456. }
  457. if (e.MouseEvent.Flags == MouseFlags.WheeledLeft)
  458. {
  459. ScrollHorizontal (-1);
  460. e.Handled = true;
  461. }
  462. }
  463. /// <summary>Gets the coordinates of the Cursor based on the SelectedCodePoint in screen coordinates</summary>
  464. public Point Cursor
  465. {
  466. get
  467. {
  468. int row = SelectedCodePoint / 16 * _rowHeight - Viewport.Y + 1;
  469. int col = SelectedCodePoint % 16 * COLUMN_WIDTH - Viewport.X + RowLabelWidth + 1; // + 1 for padding between label and first column
  470. return new (col, row);
  471. }
  472. set => throw new NotImplementedException ();
  473. }
  474. public static int MaxCodePoint = UnicodeRange.Ranges.Max (r => r.End);
  475. /// <summary>
  476. /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
  477. /// characters.
  478. /// </summary>
  479. public int SelectedCodePoint
  480. {
  481. get => _selected;
  482. set
  483. {
  484. if (_selected == value)
  485. {
  486. return;
  487. }
  488. _selected = value;
  489. if (IsInitialized)
  490. {
  491. int row = SelectedCodePoint / 16 * _rowHeight;
  492. int col = SelectedCodePoint % 16 * COLUMN_WIDTH;
  493. if (row - Viewport.Y < 0)
  494. {
  495. // Moving up.
  496. Viewport = Viewport with { Y = row };
  497. }
  498. else if (row - Viewport.Y >= Viewport.Height)
  499. {
  500. // Moving down.
  501. Viewport = Viewport with { Y = row - Viewport.Height };
  502. }
  503. int width = Viewport.Width / COLUMN_WIDTH * COLUMN_WIDTH - RowLabelWidth;
  504. if (col - Viewport.X < 0)
  505. {
  506. // Moving left.
  507. Viewport = Viewport with { X = col };
  508. }
  509. else if (col - Viewport.X >= width)
  510. {
  511. // Moving right.
  512. Viewport = Viewport with { X = col - width };
  513. }
  514. }
  515. SetNeedsDisplay ();
  516. SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null));
  517. }
  518. }
  519. public bool ShowGlyphWidths
  520. {
  521. get => _rowHeight == 2;
  522. set
  523. {
  524. _rowHeight = value ? 2 : 1;
  525. SetNeedsDisplay ();
  526. }
  527. }
  528. /// <summary>
  529. /// Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
  530. /// characters.
  531. /// </summary>
  532. public int StartCodePoint
  533. {
  534. get => _start;
  535. set
  536. {
  537. _start = value;
  538. SelectedCodePoint = value;
  539. Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
  540. SetNeedsDisplay ();
  541. }
  542. }
  543. private static int RowLabelWidth => $"U+{MaxCodePoint:x5}".Length + 1;
  544. private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16;
  545. public event EventHandler<ListViewItemEventArgs> Hover;
  546. public override void OnDrawContent (Rectangle viewport)
  547. {
  548. if (viewport.Height == 0 || viewport.Width == 0)
  549. {
  550. return;
  551. }
  552. Clear ();
  553. int cursorCol = Cursor.X + Viewport.X - RowLabelWidth - 1;
  554. int cursorRow = Cursor.Y + Viewport.Y - 1;
  555. Driver.SetAttribute (GetHotNormalColor ());
  556. Move (0, 0);
  557. Driver.AddStr (new (' ', RowLabelWidth + 1));
  558. int firstColumnX = RowLabelWidth - Viewport.X;
  559. // Header
  560. for (var hexDigit = 0; hexDigit < 16; hexDigit++)
  561. {
  562. int x = firstColumnX + hexDigit * COLUMN_WIDTH;
  563. if (x > RowLabelWidth - 2)
  564. {
  565. Move (x, 0);
  566. Driver.SetAttribute (GetHotNormalColor ());
  567. Driver.AddStr (" ");
  568. Driver.SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ());
  569. Driver.AddStr ($"{hexDigit:x}");
  570. Driver.SetAttribute (GetHotNormalColor ());
  571. Driver.AddStr (" ");
  572. }
  573. }
  574. // Even though the Clip is set to prevent us from drawing on the row potentially occupied by the horizontal
  575. // scroll bar, we do the smart thing and not actually draw that row if not necessary.
  576. for (var y = 1; y < Viewport.Height; y++)
  577. {
  578. // What row is this?
  579. int row = (y + Viewport.Y - 1) / _rowHeight;
  580. int val = row * 16;
  581. if (val > MaxCodePoint)
  582. {
  583. break;
  584. }
  585. Move (firstColumnX + COLUMN_WIDTH, y);
  586. Driver.SetAttribute (GetNormalColor ());
  587. for (var col = 0; col < 16; col++)
  588. {
  589. int x = firstColumnX + COLUMN_WIDTH * col + 1;
  590. if (x < 0 || x > Viewport.Width - 1)
  591. {
  592. continue;
  593. }
  594. Move (x, y);
  595. // If we're at the cursor position, and we don't have focus, invert the colors.
  596. if (row == cursorRow && x == cursorCol && !HasFocus)
  597. {
  598. Driver.SetAttribute (GetFocusColor ());
  599. }
  600. int scalar = val + col;
  601. var rune = (Rune)'?';
  602. if (Rune.IsValid (scalar))
  603. {
  604. rune = new (scalar);
  605. }
  606. int width = rune.GetColumns ();
  607. if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
  608. {
  609. // Draw the rune
  610. if (width > 0)
  611. {
  612. Driver.AddRune (rune);
  613. }
  614. else
  615. {
  616. if (rune.IsCombiningMark ())
  617. {
  618. // This is a hack to work around the fact that combining marks
  619. // a) can't be rendered on their own
  620. // b) that don't normalize are not properly supported in
  621. // any known terminal (esp Windows/AtlasEngine).
  622. // See Issue #2616
  623. var sb = new StringBuilder ();
  624. sb.Append ('a');
  625. sb.Append (rune);
  626. // Try normalizing after combining with 'a'. If it normalizes, at least
  627. // it'll show on the 'a'. If not, just show the replacement char.
  628. string normal = sb.ToString ().Normalize (NormalizationForm.FormC);
  629. if (normal.Length == 1)
  630. {
  631. Driver.AddRune (normal [0]);
  632. }
  633. else
  634. {
  635. Driver.AddRune (Rune.ReplacementChar);
  636. }
  637. }
  638. }
  639. }
  640. else
  641. {
  642. // Draw the width of the rune
  643. Driver.SetAttribute (ColorScheme.HotNormal);
  644. Driver.AddStr ($"{width}");
  645. }
  646. // If we're at the cursor position, and we don't have focus, revert the colors to normal
  647. if (row == cursorRow && x == cursorCol && !HasFocus)
  648. {
  649. Driver.SetAttribute (GetNormalColor ());
  650. }
  651. }
  652. // Draw row label (U+XXXX_)
  653. Move (0, y);
  654. Driver.SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal);
  655. if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
  656. {
  657. Driver.AddStr ($"U+{val / 16:x5}_ ");
  658. }
  659. else
  660. {
  661. Driver.AddStr (new (' ', RowLabelWidth));
  662. }
  663. }
  664. }
  665. public override Point? PositionCursor ()
  666. {
  667. if (HasFocus
  668. && Cursor.X >= RowLabelWidth
  669. && Cursor.X < Viewport.Width
  670. && Cursor.Y > 0
  671. && Cursor.Y < Viewport.Height)
  672. {
  673. Move (Cursor.X, Cursor.Y);
  674. }
  675. else
  676. {
  677. return null;
  678. }
  679. return Cursor;
  680. }
  681. public event EventHandler<ListViewItemEventArgs> SelectedCodePointChanged;
  682. public static string ToCamelCase (string str)
  683. {
  684. if (string.IsNullOrEmpty (str))
  685. {
  686. return str;
  687. }
  688. TextInfo textInfo = new CultureInfo ("en-US", false).TextInfo;
  689. str = textInfo.ToLower (str);
  690. str = textInfo.ToTitleCase (str);
  691. return str;
  692. }
  693. private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
  694. private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
  695. private void Handle_MouseClick (object sender, MouseEventArgs args)
  696. {
  697. MouseEvent me = args.MouseEvent;
  698. if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
  699. {
  700. return;
  701. }
  702. if (me.Position.Y == 0)
  703. {
  704. me.Position = me.Position with { Y = Cursor.Y };
  705. }
  706. if (me.Position.X < RowLabelWidth || me.Position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
  707. {
  708. me.Position = me.Position with { X = Cursor.X };
  709. }
  710. int row = (me.Position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header
  711. int col = (me.Position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
  712. if (col > 15)
  713. {
  714. col = 15;
  715. }
  716. int val = row * 16 + col;
  717. if (val > MaxCodePoint)
  718. {
  719. return;
  720. }
  721. if (me.Flags == MouseFlags.ReportMousePosition)
  722. {
  723. Hover?.Invoke (this, new (val, null));
  724. }
  725. if (!HasFocus && CanFocus)
  726. {
  727. SetFocus ();
  728. }
  729. args.Handled = true;
  730. if (me.Flags == MouseFlags.Button1Clicked)
  731. {
  732. SelectedCodePoint = val;
  733. return;
  734. }
  735. if (me.Flags == MouseFlags.Button1DoubleClicked)
  736. {
  737. SelectedCodePoint = val;
  738. ShowDetails ();
  739. return;
  740. }
  741. if (me.Flags == _contextMenu.MouseFlags)
  742. {
  743. SelectedCodePoint = val;
  744. _contextMenu = new ()
  745. {
  746. Position = new (me.Position.X + 1, me.Position.Y + 1)
  747. };
  748. MenuBarItem menuItems = new (
  749. new MenuItem []
  750. {
  751. new (
  752. "_Copy Glyph",
  753. "",
  754. CopyGlyph,
  755. null,
  756. null,
  757. (KeyCode)Key.C.WithCtrl
  758. ),
  759. new (
  760. "Copy Code _Point",
  761. "",
  762. CopyCodePoint,
  763. null,
  764. null,
  765. (KeyCode)Key.C.WithCtrl
  766. .WithShift
  767. )
  768. }
  769. );
  770. _contextMenu.Show (menuItems);
  771. }
  772. }
  773. private void ShowDetails ()
  774. {
  775. var client = new UcdApiClient ();
  776. var decResponse = string.Empty;
  777. var getCodePointError = string.Empty;
  778. var waitIndicator = new Dialog
  779. {
  780. Title = "Getting Code Point Information",
  781. X = Pos.Center (),
  782. Y = Pos.Center (),
  783. Height = 7,
  784. Width = 50,
  785. Buttons = [new () { Text = "Cancel" }]
  786. };
  787. var errorLabel = new Label
  788. {
  789. Text = UcdApiClient.BaseUrl,
  790. X = 0,
  791. Y = 1,
  792. Width = Dim.Fill (),
  793. Height = Dim.Fill (1),
  794. TextAlignment = Alignment.Center
  795. };
  796. var spinner = new SpinnerView { X = Pos.Center (), Y = Pos.Center (), Style = new Aesthetic () };
  797. spinner.AutoSpin = true;
  798. waitIndicator.Add (errorLabel);
  799. waitIndicator.Add (spinner);
  800. waitIndicator.Ready += async (s, a) =>
  801. {
  802. try
  803. {
  804. decResponse = await client.GetCodepointDec (SelectedCodePoint);
  805. Application.Invoke (() => waitIndicator.RequestStop ());
  806. }
  807. catch (HttpRequestException e)
  808. {
  809. getCodePointError = errorLabel.Text = e.Message;
  810. Application.Invoke (() => waitIndicator.RequestStop ());
  811. }
  812. };
  813. Application.Run (waitIndicator);
  814. waitIndicator.Dispose ();
  815. if (!string.IsNullOrEmpty (decResponse))
  816. {
  817. var name = string.Empty;
  818. using (JsonDocument document = JsonDocument.Parse (decResponse))
  819. {
  820. JsonElement root = document.RootElement;
  821. // Get a property by name and output its value
  822. if (root.TryGetProperty ("name", out JsonElement nameElement))
  823. {
  824. name = nameElement.GetString ();
  825. }
  826. //// Navigate to a nested property and output its value
  827. //if (root.TryGetProperty ("property3", out JsonElement property3Element)
  828. //&& property3Element.TryGetProperty ("nestedProperty", out JsonElement nestedPropertyElement)) {
  829. // Console.WriteLine (nestedPropertyElement.GetString ());
  830. //}
  831. decResponse = JsonSerializer.Serialize (
  832. document.RootElement,
  833. new
  834. JsonSerializerOptions
  835. { WriteIndented = true }
  836. );
  837. }
  838. var title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
  839. var copyGlyph = new Button { Text = "Copy _Glyph" };
  840. var copyCP = new Button { Text = "Copy Code _Point" };
  841. var cancel = new Button { Text = "Cancel" };
  842. var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] };
  843. copyGlyph.Accepting += (s, a) =>
  844. {
  845. CopyGlyph ();
  846. dlg.RequestStop ();
  847. };
  848. copyCP.Accepting += (s, a) =>
  849. {
  850. CopyCodePoint ();
  851. dlg.RequestStop ();
  852. };
  853. cancel.Accepting += (s, a) => dlg.RequestStop ();
  854. var rune = (Rune)SelectedCodePoint;
  855. var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
  856. dlg.Add (label);
  857. label = new () { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
  858. dlg.Add (label);
  859. label = new () { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
  860. dlg.Add (label);
  861. label = new () { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
  862. dlg.Add (label);
  863. label = new () { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
  864. dlg.Add (label);
  865. label = new () { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
  866. dlg.Add (label);
  867. label = new () { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
  868. dlg.Add (label);
  869. label = new () { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
  870. dlg.Add (label);
  871. label = new () { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
  872. dlg.Add (label);
  873. label = new () { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
  874. dlg.Add (label);
  875. label = new () { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
  876. dlg.Add (label);
  877. label = new () { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
  878. dlg.Add (label);
  879. label = new () { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
  880. dlg.Add (label);
  881. label = new () { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
  882. dlg.Add (label);
  883. label = new ()
  884. {
  885. Text =
  886. $"Code Point Information from {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
  887. X = 0,
  888. Y = Pos.Bottom (label)
  889. };
  890. dlg.Add (label);
  891. var json = new TextView ()
  892. {
  893. X = 0,
  894. Y = Pos.Bottom (label),
  895. Width = Dim.Fill (),
  896. Height = Dim.Fill (2),
  897. ReadOnly = true,
  898. Text = decResponse
  899. };
  900. dlg.Add (json);
  901. Application.Run (dlg);
  902. dlg.Dispose ();
  903. }
  904. else
  905. {
  906. MessageBox.ErrorQuery (
  907. "Code Point API",
  908. $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
  909. "Ok"
  910. );
  911. }
  912. // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug
  913. Application.GrabMouse (this);
  914. }
  915. }
  916. public class UcdApiClient
  917. {
  918. public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
  919. private static readonly HttpClient _httpClient = new ();
  920. public async Task<string> GetChars (string chars)
  921. {
  922. HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
  923. response.EnsureSuccessStatusCode ();
  924. return await response.Content.ReadAsStringAsync ();
  925. }
  926. public async Task<string> GetCharsName (string chars)
  927. {
  928. HttpResponseMessage response =
  929. await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
  930. response.EnsureSuccessStatusCode ();
  931. return await response.Content.ReadAsStringAsync ();
  932. }
  933. public async Task<string> GetCodepointDec (int dec)
  934. {
  935. HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
  936. response.EnsureSuccessStatusCode ();
  937. return await response.Content.ReadAsStringAsync ();
  938. }
  939. public async Task<string> GetCodepointHex (string hex)
  940. {
  941. HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
  942. response.EnsureSuccessStatusCode ();
  943. return await response.Content.ReadAsStringAsync ();
  944. }
  945. }
  946. internal class UnicodeRange
  947. {
  948. public static List<UnicodeRange> Ranges = GetRanges ();
  949. public string Category;
  950. public int End;
  951. public int Start;
  952. public UnicodeRange (int start, int end, string category)
  953. {
  954. Start = start;
  955. End = end;
  956. Category = category;
  957. }
  958. public static List<UnicodeRange> GetRanges ()
  959. {
  960. IEnumerable<UnicodeRange> ranges =
  961. from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public)
  962. let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange
  963. let name = string.IsNullOrEmpty (r.Name)
  964. ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}"
  965. : r.Name
  966. where name != "None" && name != "All"
  967. select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name);
  968. // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0
  969. List<UnicodeRange> nonBmpRanges = new ()
  970. {
  971. new (
  972. 0x1F130,
  973. 0x1F149,
  974. "Squared Latin Capital Letters"
  975. ),
  976. new (
  977. 0x12400,
  978. 0x1240f,
  979. "Cuneiform Numbers and Punctuation"
  980. ),
  981. new (0x10000, 0x1007F, "Linear B Syllabary"),
  982. new (0x10080, 0x100FF, "Linear B Ideograms"),
  983. new (0x10100, 0x1013F, "Aegean Numbers"),
  984. new (0x10300, 0x1032F, "Old Italic"),
  985. new (0x10330, 0x1034F, "Gothic"),
  986. new (0x10380, 0x1039F, "Ugaritic"),
  987. new (0x10400, 0x1044F, "Deseret"),
  988. new (0x10450, 0x1047F, "Shavian"),
  989. new (0x10480, 0x104AF, "Osmanya"),
  990. new (0x10800, 0x1083F, "Cypriot Syllabary"),
  991. new (
  992. 0x1D000,
  993. 0x1D0FF,
  994. "Byzantine Musical Symbols"
  995. ),
  996. new (0x1D100, 0x1D1FF, "Musical Symbols"),
  997. new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
  998. new (
  999. 0x1D400,
  1000. 0x1D7FF,
  1001. "Mathematical Alphanumeric Symbols"
  1002. ),
  1003. new (0x1F600, 0x1F532, "Emojis Symbols"),
  1004. new (
  1005. 0x20000,
  1006. 0x2A6DF,
  1007. "CJK Unified Ideographs Extension B"
  1008. ),
  1009. new (
  1010. 0x2F800,
  1011. 0x2FA1F,
  1012. "CJK Compatibility Ideographs Supplement"
  1013. ),
  1014. new (0xE0000, 0xE007F, "Tags")
  1015. };
  1016. return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
  1017. }
  1018. }