HexView.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. #nullable enable
  2. using System.Diagnostics;
  3. //
  4. // HexView.cs: A hexadecimal viewer
  5. //
  6. // TODO: Support searching and highlighting of the search result
  7. // TODO: Support growing/shrinking the stream (e.g. del/backspace should work).
  8. //
  9. namespace Terminal.Gui;
  10. /// <summary>An hex viewer and editor <see cref="View"/> over a <see cref="Stream"/></summary>
  11. /// <remarks>
  12. /// <para>
  13. /// <see cref="HexView"/> provides a hex editor on top of a seekable <see cref="Stream"/> with the left side
  14. /// showing an hex dump of the values in the <see cref="Stream"/> and the right side showing the contents (filtered
  15. /// to non-control sequence ASCII characters).
  16. /// </para>
  17. /// <para>Users can switch from one side to the other by using the tab key.</para>
  18. /// <para>
  19. /// To enable editing, set <see cref="AllowEdits"/> to true. When <see cref="AllowEdits"/> is true the user can
  20. /// make changes to the hexadecimal values of the <see cref="Stream"/>. Any changes are tracked in the
  21. /// <see cref="Edits"/> property (a <see cref="SortedDictionary{TKey, TValue}"/>) indicating the position where the
  22. /// changes were made and the new values. A convenience method, <see cref="ApplyEdits"/> will apply the edits to
  23. /// the <see cref="Stream"/>.
  24. /// </para>
  25. /// <para>Control the first byte shown by setting the <see cref="DisplayStart"/> property to an offset in the stream.</para>
  26. /// </remarks>
  27. public class HexView : View, IDesignable
  28. {
  29. private const int DEFAULT_ADDRESS_WIDTH = 8; // The default value for AddressWidth
  30. private const int NUM_BYTES_PER_HEX_COLUMN = 4;
  31. private const int HEX_COLUMN_WIDTH = NUM_BYTES_PER_HEX_COLUMN * 3 + 2; // 3 cols per byte + 1 for vert separator + right space
  32. private bool _firstNibble;
  33. private bool _leftSideHasFocus;
  34. private static readonly Rune _spaceCharRune = new (' ');
  35. private static readonly Rune _periodCharRune = new ('.');
  36. /// <summary>Initializes a <see cref="HexView"/> class.</summary>
  37. /// <param name="source">
  38. /// The <see cref="Stream"/> to view and edit as hex, this <see cref="Stream"/> must support seeking,
  39. /// or an exception will be thrown.
  40. /// </param>
  41. public HexView (Stream? source)
  42. {
  43. Source = source;
  44. CanFocus = true;
  45. CursorVisibility = CursorVisibility.Default;
  46. _leftSideHasFocus = true;
  47. _firstNibble = true;
  48. // PERF: Closure capture of 'this' creates a lot of overhead.
  49. // BUG: Closure capture of 'this' may have unexpected results depending on how this is called.
  50. // The above two comments apply to all of the lambdas passed to all calls to AddCommand below.
  51. // Things this view knows how to do
  52. AddCommand (Command.Left, () => MoveLeft ());
  53. AddCommand (Command.Right, () => MoveRight ());
  54. AddCommand (Command.Down, () => MoveDown (BytesPerLine));
  55. AddCommand (Command.Up, () => MoveUp (BytesPerLine));
  56. AddCommand (Command.Tab, () => Navigate (NavigationDirection.Forward));
  57. AddCommand (Command.BackTab, () => Navigate (NavigationDirection.Backward));
  58. AddCommand (Command.PageUp, () => MoveUp (BytesPerLine * Frame.Height));
  59. AddCommand (Command.PageDown, () => MoveDown (BytesPerLine * Frame.Height));
  60. AddCommand (Command.Start, () => MoveHome ());
  61. AddCommand (Command.End, () => MoveEnd ());
  62. AddCommand (Command.LeftStart, () => MoveLeftStart ());
  63. AddCommand (Command.RightEnd, () => MoveEndOfLine ());
  64. AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - _displayStart) / BytesPerLine)));
  65. AddCommand (
  66. Command.EndOfPage,
  67. () => MoveDown (BytesPerLine * (Frame.Height - 1 - (int)(Address - _displayStart) / BytesPerLine))
  68. );
  69. KeyBindings.Add (Key.CursorLeft, Command.Left);
  70. KeyBindings.Add (Key.CursorRight, Command.Right);
  71. KeyBindings.Add (Key.CursorDown, Command.Down);
  72. KeyBindings.Add (Key.CursorUp, Command.Up);
  73. KeyBindings.Add (Key.PageUp, Command.PageUp);
  74. KeyBindings.Add (Key.PageDown, Command.PageDown);
  75. KeyBindings.Add (Key.Home, Command.Start);
  76. KeyBindings.Add (Key.End, Command.End);
  77. KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.LeftStart);
  78. KeyBindings.Add (Key.CursorRight.WithCtrl, Command.RightEnd);
  79. KeyBindings.Add (Key.CursorUp.WithCtrl, Command.StartOfPage);
  80. KeyBindings.Add (Key.CursorDown.WithCtrl, Command.EndOfPage);
  81. KeyBindings.Add (Key.Tab, Command.Tab);
  82. KeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
  83. KeyBindings.Remove (Key.Space);
  84. KeyBindings.Remove (Key.Enter);
  85. LayoutComplete += HexView_LayoutComplete;
  86. }
  87. /// <summary>Initializes a <see cref="HexView"/> class.</summary>
  88. public HexView () : this (new MemoryStream ()) { }
  89. /// <summary>
  90. /// Gets or sets whether this <see cref="HexView"/> allows editing of the <see cref="Stream"/> of the underlying
  91. /// <see cref="Stream"/>.
  92. /// </summary>
  93. /// <value><c>true</c> to allow edits; otherwise, <c>false</c>.</value>
  94. public bool AllowEdits { get; set; } = true;
  95. /// <summary>Gets the current cursor position.</summary>
  96. public Point CursorPosition
  97. {
  98. get
  99. {
  100. if (_source is null || BytesPerLine == 0)
  101. {
  102. return Point.Empty;
  103. }
  104. var delta = (int)Address;
  105. if (_leftSideHasFocus)
  106. {
  107. int line = delta / BytesPerLine;
  108. int item = delta % BytesPerLine;
  109. return new (item, line);
  110. }
  111. else
  112. {
  113. int line = delta / BytesPerLine;
  114. int item = delta % BytesPerLine;
  115. return new (item, line);
  116. }
  117. }
  118. }
  119. ///<inheritdoc/>
  120. public override Point? PositionCursor ()
  121. {
  122. var delta = (int)(Address - _displayStart);
  123. int line = delta / BytesPerLine;
  124. int item = delta % BytesPerLine;
  125. int block = item / NUM_BYTES_PER_HEX_COLUMN;
  126. int column = item % NUM_BYTES_PER_HEX_COLUMN * 3;
  127. int x = GetLeftSideStartColumn () + block * HEX_COLUMN_WIDTH + column + (_firstNibble ? 0 : 1);
  128. int y = line;
  129. if (!_leftSideHasFocus)
  130. {
  131. x = GetLeftSideStartColumn () + BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH + item - 1;
  132. }
  133. Move (x, y);
  134. return new (x, y);
  135. }
  136. private SortedDictionary<long, byte> _edits = [];
  137. /// <summary>
  138. /// Gets a <see cref="SortedDictionary{TKey, TValue}"/> describing the edits done to the <see cref="HexView"/>.
  139. /// Each Key indicates an offset where an edit was made and the Value is the changed byte.
  140. /// </summary>
  141. /// <value>The edits.</value>
  142. public IReadOnlyDictionary<long, byte> Edits => _edits;
  143. private Stream? _source;
  144. /// <summary>
  145. /// Sets or gets the <see cref="Stream"/> the <see cref="HexView"/> is operating on; the stream must support
  146. /// seeking ( <see cref="Stream.CanSeek"/> == true).
  147. /// </summary>
  148. /// <value>The source.</value>
  149. public Stream? Source
  150. {
  151. get => _source;
  152. set
  153. {
  154. ArgumentNullException.ThrowIfNull (value);
  155. if (!value!.CanSeek)
  156. {
  157. throw new ArgumentException (@"The source stream must be seekable (CanSeek property)");
  158. }
  159. _source = value;
  160. if (_displayStart > _source.Length)
  161. {
  162. DisplayStart = 0;
  163. }
  164. if (Address > _source.Length)
  165. {
  166. Address = 0;
  167. }
  168. SetNeedsDisplay ();
  169. }
  170. }
  171. private int _bpl;
  172. /// <summary>The bytes length per line.</summary>
  173. public int BytesPerLine
  174. {
  175. get => _bpl;
  176. set
  177. {
  178. _bpl = value;
  179. RaisePositionChanged ();
  180. }
  181. }
  182. private long _address;
  183. /// <summary>Gets or sets the current byte position in the <see cref="Stream"/>.</summary>
  184. public long Address
  185. {
  186. get => _address;
  187. set
  188. {
  189. if (_address == value)
  190. {
  191. return;
  192. }
  193. //ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual (value, Source!.Length, $"Position");
  194. _address = value;
  195. RaisePositionChanged ();
  196. }
  197. }
  198. private long _displayStart;
  199. // TODO: Use Viewport content scrolling instead
  200. /// <summary>
  201. /// Sets or gets the offset into the <see cref="Stream"/> that will be displayed at the top of the
  202. /// <see cref="HexView"/>.
  203. /// </summary>
  204. /// <value>The display start.</value>
  205. public long DisplayStart
  206. {
  207. get => _displayStart;
  208. set
  209. {
  210. Address = value;
  211. SetDisplayStart (value);
  212. }
  213. }
  214. private int _addressWidth = DEFAULT_ADDRESS_WIDTH;
  215. /// <summary>
  216. /// Gets or sets the width of the Address column on the left. Set to 0 to hide. The default is 8.
  217. /// </summary>
  218. public int AddressWidth
  219. {
  220. get => _addressWidth;
  221. set
  222. {
  223. if (_addressWidth == value)
  224. {
  225. return;
  226. }
  227. _addressWidth = value;
  228. SetNeedsDisplay ();
  229. }
  230. }
  231. private int GetLeftSideStartColumn ()
  232. {
  233. return AddressWidth == 0 ? 0 : AddressWidth + 1;
  234. }
  235. internal void SetDisplayStart (long value)
  236. {
  237. if (value > 0 && value >= _source?.Length)
  238. {
  239. _displayStart = _source.Length - 1;
  240. }
  241. else if (value < 0)
  242. {
  243. _displayStart = 0;
  244. }
  245. else
  246. {
  247. _displayStart = value;
  248. }
  249. SetNeedsDisplay ();
  250. }
  251. /// <summary>
  252. /// Applies and edits made to the <see cref="Stream"/> and resets the contents of the
  253. /// <see cref="Edits"/> property.
  254. /// </summary>
  255. /// <param name="stream">If provided also applies the changes to the passed <see cref="Stream"/>.</param>
  256. /// .
  257. public void ApplyEdits (Stream? stream = null)
  258. {
  259. foreach (KeyValuePair<long, byte> kv in _edits)
  260. {
  261. _source!.Position = kv.Key;
  262. _source.WriteByte (kv.Value);
  263. _source.Flush ();
  264. if (stream is { })
  265. {
  266. stream.Position = kv.Key;
  267. stream.WriteByte (kv.Value);
  268. stream.Flush ();
  269. }
  270. }
  271. _edits = new ();
  272. SetNeedsDisplay ();
  273. }
  274. /// <summary>
  275. /// Discards the edits made to the <see cref="Stream"/> by resetting the contents of the
  276. /// <see cref="Edits"/> property.
  277. /// </summary>
  278. public void DiscardEdits () { _edits = new (); }
  279. /// <inheritdoc/>
  280. protected internal override bool OnMouseEvent (MouseEvent me)
  281. {
  282. if (_source is null)
  283. {
  284. return false;
  285. }
  286. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
  287. && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  288. && !me.Flags.HasFlag (MouseFlags.WheeledDown)
  289. && !me.Flags.HasFlag (MouseFlags.WheeledUp))
  290. {
  291. return false;
  292. }
  293. if (!HasFocus)
  294. {
  295. SetFocus ();
  296. }
  297. if (me.Flags == MouseFlags.WheeledDown)
  298. {
  299. DisplayStart = Math.Min (DisplayStart + BytesPerLine, _source.Length);
  300. return true;
  301. }
  302. if (me.Flags == MouseFlags.WheeledUp)
  303. {
  304. DisplayStart = Math.Max (DisplayStart - BytesPerLine, 0);
  305. return true;
  306. }
  307. if (me.Position.X < GetLeftSideStartColumn ())
  308. {
  309. return true;
  310. }
  311. int nblocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
  312. int blocksSize = nblocks * HEX_COLUMN_WIDTH;
  313. int blocksRightOffset = GetLeftSideStartColumn () + blocksSize - 1;
  314. if (me.Position.X > blocksRightOffset + BytesPerLine - 1)
  315. {
  316. return true;
  317. }
  318. bool clickIsOnLeftSide = me.Position.X >= blocksRightOffset;
  319. long lineStart = me.Position.Y * BytesPerLine + _displayStart;
  320. int x = me.Position.X - GetLeftSideStartColumn () + 1;
  321. int block = x / HEX_COLUMN_WIDTH;
  322. x -= block * 2;
  323. int empty = x % 3;
  324. int item = x / 3;
  325. if (!clickIsOnLeftSide && item > 0 && (empty == 0 || x == block * HEX_COLUMN_WIDTH + HEX_COLUMN_WIDTH - 1 - block * 2))
  326. {
  327. return true;
  328. }
  329. _firstNibble = true;
  330. if (clickIsOnLeftSide)
  331. {
  332. Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, _source.Length - 1);
  333. }
  334. else
  335. {
  336. Address = Math.Min (lineStart + item, _source.Length - 1);
  337. }
  338. if (me.Flags == MouseFlags.Button1DoubleClicked)
  339. {
  340. _leftSideHasFocus = !clickIsOnLeftSide;
  341. if (_leftSideHasFocus)
  342. {
  343. _firstNibble = empty == 1;
  344. }
  345. else
  346. {
  347. _firstNibble = true;
  348. }
  349. }
  350. SetNeedsDisplay ();
  351. return true;
  352. }
  353. ///<inheritdoc/>
  354. public override void OnDrawContent (Rectangle viewport)
  355. {
  356. if (Source is null)
  357. {
  358. return;
  359. }
  360. Attribute currentAttribute;
  361. Attribute current = GetFocusColor ();
  362. Driver.SetAttribute (current);
  363. Move (0, 0);
  364. int nblocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
  365. var data = new byte [nblocks * NUM_BYTES_PER_HEX_COLUMN * viewport.Height];
  366. Source.Position = _displayStart;
  367. int n = _source.Read (data, 0, data.Length);
  368. Attribute activeColor = GetHotNormalColor ();
  369. Attribute trackingColor = GetHotFocusColor ();
  370. for (var line = 0; line < viewport.Height; line++)
  371. {
  372. Rectangle lineRect = new (0, line, viewport.Width, 1);
  373. if (!Viewport.Contains (lineRect))
  374. {
  375. continue;
  376. }
  377. Move (0, line);
  378. currentAttribute = GetHotNormalColor ();
  379. Driver.SetAttribute (currentAttribute);
  380. string address = $"{_displayStart + line * nblocks * NUM_BYTES_PER_HEX_COLUMN:x8}";
  381. Driver.AddStr ($"{address.Substring (8 - AddressWidth)}");
  382. if (AddressWidth > 0)
  383. {
  384. Driver.AddStr (" ");
  385. }
  386. SetAttribute (GetNormalColor ());
  387. for (var block = 0; block < nblocks; block++)
  388. {
  389. for (var b = 0; b < NUM_BYTES_PER_HEX_COLUMN; b++)
  390. {
  391. int offset = line * nblocks * NUM_BYTES_PER_HEX_COLUMN + block * NUM_BYTES_PER_HEX_COLUMN + b;
  392. byte value = GetData (data, offset, out bool edited);
  393. if (offset + _displayStart == Address || edited)
  394. {
  395. SetAttribute (_leftSideHasFocus ? activeColor : trackingColor);
  396. }
  397. else
  398. {
  399. SetAttribute (GetNormalColor ());
  400. }
  401. Driver.AddStr (offset >= n && !edited ? " " : $"{value:x2}");
  402. SetAttribute (GetNormalColor ());
  403. Driver.AddRune (_spaceCharRune);
  404. }
  405. Driver.AddStr (block + 1 == nblocks ? " " : "| ");
  406. }
  407. for (var bitem = 0; bitem < nblocks * NUM_BYTES_PER_HEX_COLUMN; bitem++)
  408. {
  409. int offset = line * nblocks * NUM_BYTES_PER_HEX_COLUMN + bitem;
  410. byte b = GetData (data, offset, out bool edited);
  411. Rune c;
  412. if (offset >= n && !edited)
  413. {
  414. c = _spaceCharRune;
  415. }
  416. else
  417. {
  418. if (b < 32)
  419. {
  420. c = _periodCharRune;
  421. }
  422. else if (b > 127)
  423. {
  424. c = _periodCharRune;
  425. }
  426. else
  427. {
  428. Rune.DecodeFromUtf8 (new (ref b), out c, out _);
  429. }
  430. }
  431. if (offset + _displayStart == Address || edited)
  432. {
  433. SetAttribute (_leftSideHasFocus ? trackingColor : activeColor);
  434. }
  435. else
  436. {
  437. SetAttribute (GetNormalColor ());
  438. }
  439. Driver.AddRune (c);
  440. }
  441. }
  442. void SetAttribute (Attribute attribute)
  443. {
  444. if (currentAttribute != attribute)
  445. {
  446. currentAttribute = attribute;
  447. Driver.SetAttribute (attribute);
  448. }
  449. }
  450. }
  451. /// <summary>Raises the <see cref="Edited"/> event.</summary>
  452. protected void RaiseEdited (HexViewEditEventArgs e)
  453. {
  454. OnEditied (e);
  455. Edited?.Invoke (this, e);
  456. }
  457. /// <summary>Event to be invoked when an edit is made on the <see cref="Stream"/>.</summary>
  458. public event EventHandler<HexViewEditEventArgs>? Edited;
  459. /// <summary>
  460. ///
  461. /// </summary>
  462. /// <param name="e"></param>
  463. protected virtual void OnEditied (HexViewEditEventArgs e) { }
  464. /// <summary>Raises the <see cref="PositionChanged"/> event.</summary>
  465. protected void RaisePositionChanged ()
  466. {
  467. HexViewEventArgs args = new (Address, CursorPosition, BytesPerLine);
  468. OnPositionChanged (args);
  469. PositionChanged?.Invoke (this, args);
  470. }
  471. /// <summary>
  472. /// Called when <see cref="Address"/> has changed.
  473. /// </summary>
  474. protected virtual void OnPositionChanged (HexViewEventArgs e) { }
  475. /// <summary>Event to be invoked when the position and cursor position changes.</summary>
  476. public event EventHandler<HexViewEventArgs>? PositionChanged;
  477. /// <inheritdoc/>
  478. public override bool OnProcessKeyDown (Key keyEvent)
  479. {
  480. if (!AllowEdits || _source is null)
  481. {
  482. return false;
  483. }
  484. // Ignore control characters and other special keys
  485. if (keyEvent < Key.Space || keyEvent.KeyCode > KeyCode.CharMask)
  486. {
  487. return false;
  488. }
  489. if (_leftSideHasFocus)
  490. {
  491. int value;
  492. var k = (char)keyEvent.KeyCode;
  493. if (k is >= 'A' and <= 'F')
  494. {
  495. value = k - 'A' + 10;
  496. }
  497. else if (k is >= 'a' and <= 'f')
  498. {
  499. value = k - 'a' + 10;
  500. }
  501. else if (k is >= '0' and <= '9')
  502. {
  503. value = k - '0';
  504. }
  505. else
  506. {
  507. return false;
  508. }
  509. byte b;
  510. if (!_edits.TryGetValue (Address, out b))
  511. {
  512. _source.Position = Address;
  513. b = (byte)_source.ReadByte ();
  514. }
  515. // BUGBUG: This makes no sense here.
  516. RedisplayLine (Address);
  517. if (_firstNibble)
  518. {
  519. _firstNibble = false;
  520. b = (byte)((b & 0xf) | (value << NUM_BYTES_PER_HEX_COLUMN));
  521. _edits [Address] = b;
  522. RaiseEdited (new (Address, _edits [Address]));
  523. }
  524. else
  525. {
  526. b = (byte)((b & 0xf0) | value);
  527. _edits [Address] = b;
  528. RaiseEdited (new (Address, _edits [Address]));
  529. MoveRight ();
  530. }
  531. return true;
  532. }
  533. else
  534. {
  535. Rune r = keyEvent.AsRune;
  536. // TODO: Enable entering Tab char - somehow disable Tab for navigation
  537. _edits [Address] = (byte)(r.Value & 0x00FF);
  538. MoveRight ();
  539. if ((byte)(r.Value & 0xFF00) > 0)
  540. {
  541. _edits [Address] = (byte)(r.Value & 0xFF00);
  542. MoveRight ();
  543. }
  544. //RaiseEdited (new (Address, _edits [Address]));
  545. }
  546. return false;
  547. }
  548. //
  549. // This is used to support editing of the buffer on a peer List<>,
  550. // the offset corresponds to an offset relative to DisplayStart, and
  551. // the buffer contains the contents of a screenful of data, so the
  552. // offset is relative to the buffer.
  553. //
  554. //
  555. private byte GetData (byte [] buffer, int offset, out bool edited)
  556. {
  557. long pos = DisplayStart + offset;
  558. if (_edits.TryGetValue (pos, out byte v))
  559. {
  560. edited = true;
  561. return v;
  562. }
  563. edited = false;
  564. return buffer [offset];
  565. }
  566. private void HexView_LayoutComplete (object? sender, LayoutEventArgs e)
  567. {
  568. // Small buffers will just show the position, with the bsize field value (4 bytes)
  569. BytesPerLine = NUM_BYTES_PER_HEX_COLUMN;
  570. if (Viewport.Width - GetLeftSideStartColumn () > 17)
  571. {
  572. BytesPerLine = NUM_BYTES_PER_HEX_COLUMN * ((Viewport.Width - GetLeftSideStartColumn ()) / 18);
  573. }
  574. }
  575. private bool MoveDown (int bytes)
  576. {
  577. RedisplayLine (Address);
  578. if (Address + bytes < _source.Length)
  579. {
  580. Address += bytes;
  581. }
  582. else if ((bytes == BytesPerLine * Viewport.Height && _source.Length >= DisplayStart + BytesPerLine * Viewport.Height)
  583. || (bytes <= BytesPerLine * Viewport.Height - BytesPerLine
  584. && _source.Length <= DisplayStart + BytesPerLine * Viewport.Height))
  585. {
  586. long p = Address;
  587. while (p + BytesPerLine < _source.Length)
  588. {
  589. p += BytesPerLine;
  590. }
  591. Address = p;
  592. }
  593. if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
  594. {
  595. SetDisplayStart (DisplayStart + bytes);
  596. SetNeedsDisplay ();
  597. }
  598. else
  599. {
  600. RedisplayLine (Address);
  601. }
  602. return true;
  603. }
  604. private bool MoveEnd ()
  605. {
  606. Address = _source!.Length;
  607. if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
  608. {
  609. SetDisplayStart (Address);
  610. SetNeedsDisplay ();
  611. }
  612. else
  613. {
  614. RedisplayLine (Address);
  615. }
  616. return true;
  617. }
  618. private bool MoveEndOfLine ()
  619. {
  620. Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, _source!.Length);
  621. SetNeedsDisplay ();
  622. return true;
  623. }
  624. private bool MoveHome ()
  625. {
  626. DisplayStart = 0;
  627. SetNeedsDisplay ();
  628. return true;
  629. }
  630. private bool MoveLeft ()
  631. {
  632. RedisplayLine (Address);
  633. if (_leftSideHasFocus)
  634. {
  635. if (!_firstNibble)
  636. {
  637. _firstNibble = true;
  638. return true;
  639. }
  640. _firstNibble = false;
  641. }
  642. if (Address == 0)
  643. {
  644. return true;
  645. }
  646. if (Address - 1 < DisplayStart)
  647. {
  648. SetDisplayStart (_displayStart - BytesPerLine);
  649. SetNeedsDisplay ();
  650. }
  651. else
  652. {
  653. RedisplayLine (Address);
  654. }
  655. Address--;
  656. return true;
  657. }
  658. private bool MoveRight ()
  659. {
  660. RedisplayLine (Address);
  661. if (_leftSideHasFocus)
  662. {
  663. if (_firstNibble)
  664. {
  665. _firstNibble = false;
  666. return true;
  667. }
  668. _firstNibble = true;
  669. }
  670. if (Address < _source.Length - 1)
  671. {
  672. Address++;
  673. }
  674. if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
  675. {
  676. SetDisplayStart (DisplayStart + BytesPerLine);
  677. SetNeedsDisplay ();
  678. }
  679. else
  680. {
  681. RedisplayLine (Address);
  682. }
  683. return true;
  684. }
  685. private bool MoveLeftStart ()
  686. {
  687. Address = Address / BytesPerLine * BytesPerLine;
  688. SetNeedsDisplay ();
  689. return true;
  690. }
  691. private bool MoveUp (int bytes)
  692. {
  693. RedisplayLine (Address);
  694. if (Address - bytes > -1)
  695. {
  696. Address -= bytes;
  697. }
  698. if (Address < DisplayStart)
  699. {
  700. SetDisplayStart (DisplayStart - bytes);
  701. SetNeedsDisplay ();
  702. }
  703. else
  704. {
  705. RedisplayLine (Address);
  706. }
  707. return true;
  708. }
  709. private void RedisplayLine (long pos)
  710. {
  711. if (BytesPerLine == 0)
  712. {
  713. return;
  714. }
  715. var delta = (int)(pos - DisplayStart);
  716. int line = delta / BytesPerLine;
  717. SetNeedsDisplay (new (0, line, Viewport.Width, 1));
  718. }
  719. private bool Navigate (NavigationDirection direction)
  720. {
  721. switch (direction)
  722. {
  723. case NavigationDirection.Forward:
  724. _leftSideHasFocus = !_leftSideHasFocus;
  725. RedisplayLine (Address);
  726. _firstNibble = true;
  727. return true;
  728. case NavigationDirection.Backward:
  729. _leftSideHasFocus = !_leftSideHasFocus;
  730. RedisplayLine (Address);
  731. _firstNibble = true;
  732. return true;
  733. }
  734. return false;
  735. }
  736. /// <inheritdoc/>
  737. bool IDesignable.EnableForDesign ()
  738. {
  739. Source = new MemoryStream (Encoding.UTF8.GetBytes ("HexView data with wide codepoints: 𝔹Aℝ𝔽!"));
  740. return true;
  741. }
  742. }