CharacterMap.cs 39 KB

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