CharacterMap.cs 27 KB

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