InteractiveLineControl.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.ComponentModel;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.Globalization;
  7. using Avalonia;
  8. using Avalonia.Controls;
  9. using Avalonia.Controls.Documents;
  10. using Avalonia.Controls.Primitives;
  11. using Avalonia.Media;
  12. using Avalonia.Media.TextFormatting;
  13. namespace TextTestApp
  14. {
  15. public class InteractiveLineControl : Control
  16. {
  17. /// <summary>
  18. /// Defines the <see cref="Text" /> property.
  19. /// </summary>
  20. public static readonly StyledProperty<string?> TextProperty =
  21. TextBlock.TextProperty.AddOwner<InteractiveLineControl>();
  22. /// <summary>
  23. /// Defines the <see cref="Background"/> property.
  24. /// </summary>
  25. public static readonly StyledProperty<IBrush?> BackgroundProperty =
  26. Border.BackgroundProperty.AddOwner<InteractiveLineControl>();
  27. public static readonly StyledProperty<IBrush?> ExtentStrokeProperty =
  28. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(ExtentStroke));
  29. public static readonly StyledProperty<IBrush?> BaselineStrokeProperty =
  30. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BaselineStroke));
  31. public static readonly StyledProperty<IBrush?> TextBoundsStrokeProperty =
  32. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(TextBoundsStroke));
  33. public static readonly StyledProperty<IBrush?> RunBoundsStrokeProperty =
  34. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(RunBoundsStroke));
  35. public static readonly StyledProperty<IBrush?> NextHitStrokeProperty =
  36. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(NextHitStroke));
  37. public static readonly StyledProperty<IBrush?> BackspaceHitStrokeProperty =
  38. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BackspaceHitStroke));
  39. public static readonly StyledProperty<IBrush?> PreviousHitStrokeProperty =
  40. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(PreviousHitStroke));
  41. public static readonly StyledProperty<IBrush?> DistanceStrokeProperty =
  42. AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(DistanceStroke));
  43. public IBrush? ExtentStroke
  44. {
  45. get => GetValue(ExtentStrokeProperty);
  46. set => SetValue(ExtentStrokeProperty, value);
  47. }
  48. public IBrush? BaselineStroke
  49. {
  50. get => GetValue(BaselineStrokeProperty);
  51. set => SetValue(BaselineStrokeProperty, value);
  52. }
  53. public IBrush? TextBoundsStroke
  54. {
  55. get => GetValue(TextBoundsStrokeProperty);
  56. set => SetValue(TextBoundsStrokeProperty, value);
  57. }
  58. public IBrush? RunBoundsStroke
  59. {
  60. get => GetValue(RunBoundsStrokeProperty);
  61. set => SetValue(RunBoundsStrokeProperty, value);
  62. }
  63. public IBrush? NextHitStroke
  64. {
  65. get => GetValue(NextHitStrokeProperty);
  66. set => SetValue(NextHitStrokeProperty, value);
  67. }
  68. public IBrush? BackspaceHitStroke
  69. {
  70. get => GetValue(BackspaceHitStrokeProperty);
  71. set => SetValue(BackspaceHitStrokeProperty, value);
  72. }
  73. public IBrush? PreviousHitStroke
  74. {
  75. get => GetValue(PreviousHitStrokeProperty);
  76. set => SetValue(PreviousHitStrokeProperty, value);
  77. }
  78. public IBrush? DistanceStroke
  79. {
  80. get => GetValue(DistanceStrokeProperty);
  81. set => SetValue(DistanceStrokeProperty, value);
  82. }
  83. private IPen? _extentPen;
  84. protected IPen ExtentPen => _extentPen ??= new Pen(ExtentStroke, dashStyle: DashStyle.Dash);
  85. private IPen? _baselinePen;
  86. protected IPen BaselinePen => _baselinePen ??= new Pen(BaselineStroke);
  87. private IPen? _textBoundsPen;
  88. protected IPen TextBoundsPen => _textBoundsPen ??= new Pen(TextBoundsStroke);
  89. private IPen? _runBoundsPen;
  90. protected IPen RunBoundsPen => _runBoundsPen ??= new Pen(RunBoundsStroke, dashStyle: DashStyle.Dash);
  91. private IPen? _nextHitPen;
  92. protected IPen NextHitPen => _nextHitPen ??= new Pen(NextHitStroke);
  93. private IPen? _previousHitPen;
  94. protected IPen PreviousHitPen => _previousHitPen ??= new Pen(PreviousHitStroke);
  95. private IPen? _backspaceHitPen;
  96. protected IPen BackspaceHitPen => _backspaceHitPen ??= new Pen(BackspaceHitStroke);
  97. private IPen? _distancePen;
  98. protected IPen DistancePen => _distancePen ??= new Pen(DistanceStroke);
  99. /// <summary>
  100. /// Gets or sets the text to draw.
  101. /// </summary>
  102. public string? Text
  103. {
  104. get => GetValue(TextProperty);
  105. set => SetValue(TextProperty, value);
  106. }
  107. /// <summary>
  108. /// Gets or sets a brush used to paint the control's background.
  109. /// </summary>
  110. public IBrush? Background
  111. {
  112. get => GetValue(BackgroundProperty);
  113. set => SetValue(BackgroundProperty, value);
  114. }
  115. // TextRunProperties
  116. /// <summary>
  117. /// Defines the <see cref="FontFamily"/> property.
  118. /// </summary>
  119. public static readonly StyledProperty<FontFamily> FontFamilyProperty =
  120. TextElement.FontFamilyProperty.AddOwner<InteractiveLineControl>();
  121. /// <summary>
  122. /// Defines the <see cref="FontFeaturesProperty"/> property.
  123. /// </summary>
  124. public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty =
  125. TextElement.FontFeaturesProperty.AddOwner<InteractiveLineControl>();
  126. /// <summary>
  127. /// Defines the <see cref="FontSize"/> property.
  128. /// </summary>
  129. public static readonly StyledProperty<double> FontSizeProperty =
  130. TextElement.FontSizeProperty.AddOwner<InteractiveLineControl>();
  131. /// <summary>
  132. /// Defines the <see cref="FontStyle"/> property.
  133. /// </summary>
  134. public static readonly StyledProperty<FontStyle> FontStyleProperty =
  135. TextElement.FontStyleProperty.AddOwner<InteractiveLineControl>();
  136. /// <summary>
  137. /// Defines the <see cref="FontWeight"/> property.
  138. /// </summary>
  139. public static readonly StyledProperty<FontWeight> FontWeightProperty =
  140. TextElement.FontWeightProperty.AddOwner<InteractiveLineControl>();
  141. /// <summary>
  142. /// Defines the <see cref="FontWeight"/> property.
  143. /// </summary>
  144. public static readonly StyledProperty<FontStretch> FontStretchProperty =
  145. TextElement.FontStretchProperty.AddOwner<InteractiveLineControl>();
  146. /// <summary>
  147. /// Gets or sets the font family used to draw the control's text.
  148. /// </summary>
  149. public FontFamily FontFamily
  150. {
  151. get => GetValue(FontFamilyProperty);
  152. set => SetValue(FontFamilyProperty, value);
  153. }
  154. /// <summary>
  155. /// Gets or sets the font features turned on/off.
  156. /// </summary>
  157. public FontFeatureCollection? FontFeatures
  158. {
  159. get => GetValue(FontFeaturesProperty);
  160. set => SetValue(FontFeaturesProperty, value);
  161. }
  162. /// <summary>
  163. /// Gets or sets the size of the control's text in points.
  164. /// </summary>
  165. public double FontSize
  166. {
  167. get => GetValue(FontSizeProperty);
  168. set => SetValue(FontSizeProperty, value);
  169. }
  170. /// <summary>
  171. /// Gets or sets the font style used to draw the control's text.
  172. /// </summary>
  173. public FontStyle FontStyle
  174. {
  175. get => GetValue(FontStyleProperty);
  176. set => SetValue(FontStyleProperty, value);
  177. }
  178. /// <summary>
  179. /// Gets or sets the font weight used to draw the control's text.
  180. /// </summary>
  181. public FontWeight FontWeight
  182. {
  183. get => GetValue(FontWeightProperty);
  184. set => SetValue(FontWeightProperty, value);
  185. }
  186. /// <summary>
  187. /// Gets or sets the font stretch used to draw the control's text.
  188. /// </summary>
  189. public FontStretch FontStretch
  190. {
  191. get => GetValue(FontStretchProperty);
  192. set => SetValue(FontStretchProperty, value);
  193. }
  194. private GenericTextRunProperties? _textRunProperties;
  195. public GenericTextRunProperties TextRunProperties
  196. {
  197. get
  198. {
  199. return _textRunProperties ??= CreateTextRunProperties();
  200. }
  201. set
  202. {
  203. if (value == null)
  204. throw new ArgumentNullException(nameof(value));
  205. _textRunProperties = value;
  206. SetCurrentValue(FontFamilyProperty, value.Typeface.FontFamily);
  207. SetCurrentValue(FontFeaturesProperty, value.FontFeatures);
  208. SetCurrentValue(FontSizeProperty, value.FontRenderingEmSize);
  209. SetCurrentValue(FontStyleProperty, value.Typeface.Style);
  210. SetCurrentValue(FontWeightProperty, value.Typeface.Weight);
  211. SetCurrentValue(FontStretchProperty, value.Typeface.Stretch);
  212. }
  213. }
  214. private GenericTextRunProperties CreateTextRunProperties()
  215. {
  216. Typeface typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
  217. return new GenericTextRunProperties(typeface, FontFeatures, FontSize,
  218. textDecorations: null,
  219. foregroundBrush: Brushes.Black,
  220. backgroundBrush: null,
  221. baselineAlignment: BaselineAlignment.Baseline,
  222. cultureInfo: null);
  223. }
  224. // TextParagraphProperties
  225. private GenericTextParagraphProperties? _textParagraphProperties;
  226. public GenericTextParagraphProperties TextParagraphProperties
  227. {
  228. get
  229. {
  230. return _textParagraphProperties ??= CreateTextParagraphProperties();
  231. }
  232. set
  233. {
  234. if (value == null)
  235. throw new ArgumentNullException(nameof(value));
  236. _textParagraphProperties = null;
  237. SetCurrentValue(FlowDirectionProperty, value.FlowDirection);
  238. }
  239. }
  240. private GenericTextParagraphProperties CreateTextParagraphProperties()
  241. {
  242. return new GenericTextParagraphProperties(
  243. FlowDirection,
  244. TextAlignment.Start,
  245. firstLineInParagraph: false,
  246. alwaysCollapsible: false,
  247. TextRunProperties,
  248. textWrapping: TextWrapping.NoWrap,
  249. lineHeight: 0,
  250. indent: 0,
  251. letterSpacing: 0);
  252. }
  253. private readonly ITextSource _textSource;
  254. private class TextSource : ITextSource
  255. {
  256. private readonly InteractiveLineControl _owner;
  257. public TextSource(InteractiveLineControl owner)
  258. {
  259. _owner = owner;
  260. }
  261. public TextRun? GetTextRun(int textSourceIndex)
  262. {
  263. string text = _owner.Text ?? string.Empty;
  264. if (textSourceIndex < 0 || textSourceIndex >= text.Length)
  265. return null;
  266. return new TextCharacters(text, _owner.TextRunProperties);
  267. }
  268. }
  269. private TextLine? _textLine;
  270. public TextLine? TextLine => _textLine ??= TextFormatter.Current.FormatLine(_textSource, 0, Bounds.Size.Width, TextParagraphProperties);
  271. private TextLayout? _textLayout;
  272. public TextLayout TextLayout => _textLayout ??= new TextLayout(_textSource, TextParagraphProperties);
  273. private Size? _textLineSize;
  274. protected Size TextLineSize => _textLineSize ??= TextLine is { } textLine ? new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height) : default;
  275. private Size? _inkSize;
  276. protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(-textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace - textLine.OverhangTrailing, textLine.Extent) : default;
  277. public event EventHandler? TextLineChanged;
  278. public InteractiveLineControl()
  279. {
  280. _textSource = new TextSource(this);
  281. RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
  282. RenderOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias);
  283. }
  284. private void InvalidateTextRunProperties()
  285. {
  286. _textRunProperties = null;
  287. InvalidateTextParagraphProperties();
  288. }
  289. private void InvalidateTextParagraphProperties()
  290. {
  291. _textParagraphProperties = null;
  292. InvalidateTextLine();
  293. }
  294. private void InvalidateTextLine()
  295. {
  296. _textLayout = null;
  297. _textLine = null;
  298. _textLineSize = null;
  299. _inkSize = null;
  300. InvalidateMeasure();
  301. InvalidateVisual();
  302. TextLineChanged?.Invoke(this, EventArgs.Empty);
  303. }
  304. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
  305. {
  306. base.OnPropertyChanged(change);
  307. switch (change.Property.Name)
  308. {
  309. case nameof(FontFamily):
  310. case nameof(FontSize):
  311. InvalidateTextRunProperties();
  312. break;
  313. case nameof(FontFeatures):
  314. if (change.OldValue is FontFeatureCollection oc)
  315. oc.CollectionChanged -= OnFeatureCollectionChanged;
  316. if (change.NewValue is FontFeatureCollection nc)
  317. nc.CollectionChanged += OnFeatureCollectionChanged;
  318. OnFeatureCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  319. break;
  320. case nameof(FontStyle):
  321. case nameof(FontWeight):
  322. case nameof(FontStretch):
  323. InvalidateTextRunProperties();
  324. break;
  325. case nameof(FlowDirection):
  326. InvalidateTextParagraphProperties();
  327. break;
  328. case nameof(Text):
  329. InvalidateTextLine();
  330. break;
  331. case nameof(BaselineStroke):
  332. _baselinePen = null;
  333. InvalidateVisual();
  334. break;
  335. case nameof(TextBoundsStroke):
  336. _textBoundsPen = null;
  337. InvalidateVisual();
  338. break;
  339. case nameof(RunBoundsStroke):
  340. _runBoundsPen = null;
  341. InvalidateVisual();
  342. break;
  343. case nameof(NextHitStroke):
  344. _nextHitPen = null;
  345. InvalidateVisual();
  346. break;
  347. case nameof(PreviousHitStroke):
  348. _previousHitPen = null;
  349. InvalidateVisual();
  350. break;
  351. case nameof(BackspaceHitStroke):
  352. _backspaceHitPen = null;
  353. InvalidateVisual();
  354. break;
  355. }
  356. base.OnPropertyChanged(change);
  357. }
  358. private void OnFeatureCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
  359. {
  360. InvalidateTextRunProperties();
  361. }
  362. protected override Size MeasureOverride(Size availableSize)
  363. {
  364. if (TextLine == null)
  365. return default;
  366. return new Size(Math.Max(TextLineSize.Width, InkSize.Width), Math.Max(TextLineSize.Height, InkSize.Height));
  367. }
  368. private const double VerticalSpacing = 5;
  369. private const double HorizontalSpacing = 5;
  370. private const double ArrowSize = 5;
  371. private const double LabelFontSize = 9;
  372. private Dictionary<string, FormattedText> _labelsCache = new();
  373. protected FormattedText GetOrCreateLabel(string label, IBrush brush, bool disableCache = false)
  374. {
  375. if (_labelsCache.TryGetValue(label, out var text))
  376. return text;
  377. text = new FormattedText(label, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, Typeface.Default, LabelFontSize, brush);
  378. if (!disableCache)
  379. _labelsCache[label] = text;
  380. return text;
  381. }
  382. private Rect _inkRenderBounds;
  383. private Rect _lineRenderBounds;
  384. public Rect InkRenderBounds => _inkRenderBounds;
  385. public Rect LineRenderBounds => _lineRenderBounds;
  386. public override void Render(DrawingContext context)
  387. {
  388. TextLine? textLine = TextLine;
  389. if (textLine == null)
  390. return;
  391. // overhang leading should be negative when extending (e.g. for j) WPF: "When the leading alignment point comes before the leading drawn pixel, the value is negative." - docs wrong but values correct
  392. // overhang trailing should be negative when extending (e.g. for f) WPF: "The OverhangTrailing value will be positive when the trailing drawn pixel comes before the trailing alignment point."
  393. // overhang after should be negative when inside (e.g. for x) WPF: "The value is positive if the bottommost drawn pixel goes below the line bottom, and is negative if it is within (on or above) the line."
  394. // => we want overhang before to be negative when inside (e.g. for x)
  395. double overhangBefore = textLine.Extent - textLine.OverhangAfter - textLine.Height;
  396. Rect inkBounds = new Rect(new Point(textLine.OverhangLeading, -overhangBefore), InkSize);
  397. Rect lineBounds = new Rect(new Point(0, 0), TextLineSize);
  398. if (inkBounds.Left < 0)
  399. lineBounds = lineBounds.Translate(new Vector(-inkBounds.Left, 0));
  400. if (inkBounds.Top < 0)
  401. lineBounds = lineBounds.Translate(new Vector(0, -inkBounds.Top));
  402. _inkRenderBounds = inkBounds;
  403. _lineRenderBounds = lineBounds;
  404. Rect bounds = new Rect(0, 0, Math.Max(inkBounds.Right, lineBounds.Right), Math.Max(inkBounds.Bottom, lineBounds.Bottom));
  405. double labelX = bounds.Right + HorizontalSpacing;
  406. if (Background is IBrush background)
  407. context.FillRectangle(background, lineBounds);
  408. if (ExtentStroke != null)
  409. {
  410. context.DrawRectangle(ExtentPen, inkBounds);
  411. RenderLabel(context, nameof(textLine.Extent), ExtentStroke, labelX, inkBounds.Top);
  412. }
  413. using (context.PushTransform(Matrix.CreateTranslation(lineBounds.Left, lineBounds.Top)))
  414. {
  415. labelX -= lineBounds.Left; // labels to ignore horizontal transform
  416. if (BaselineStroke != null)
  417. {
  418. RenderFontLine(context, textLine.Baseline, lineBounds.Width, BaselinePen); // no other lines currently available in Avalonia
  419. RenderLabel(context, nameof(textLine.Baseline), BaselineStroke, labelX, textLine.Baseline);
  420. }
  421. textLine.Draw(context, lineOrigin: default);
  422. var runBoundsStroke = RunBoundsStroke;
  423. if (TextBoundsStroke != null || runBoundsStroke != null)
  424. {
  425. IReadOnlyList<TextBounds> textBounds = textLine.GetTextBounds(textLine.FirstTextSourceIndex, textLine.Length);
  426. foreach (var textBound in textBounds)
  427. {
  428. if (runBoundsStroke != null)
  429. {
  430. var runBounds = textBound.TextRunBounds;
  431. foreach (var runBound in runBounds)
  432. context.DrawRectangle(RunBoundsPen, runBound.Rectangle);
  433. }
  434. context.DrawRectangle(TextBoundsPen, textBound.Rectangle);
  435. }
  436. }
  437. double y = Math.Max(inkBounds.Bottom, lineBounds.Bottom) + VerticalSpacing * 2;
  438. if (NextHitStroke != null)
  439. {
  440. RenderHits(context, NextHitPen, textLine, textLine.GetNextCaretCharacterHit, new CharacterHit(0), ref y);
  441. RenderLabel(context, nameof(textLine.GetNextCaretCharacterHit), NextHitStroke, labelX, y);
  442. y += VerticalSpacing * 2;
  443. }
  444. if (PreviousHitStroke != null)
  445. {
  446. RenderLabel(context, nameof(textLine.GetPreviousCaretCharacterHit), PreviousHitStroke, labelX, y);
  447. RenderHits(context, PreviousHitPen, textLine, textLine.GetPreviousCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
  448. y += VerticalSpacing * 2;
  449. }
  450. if (BackspaceHitStroke != null)
  451. {
  452. RenderLabel(context, nameof(textLine.GetBackspaceCaretCharacterHit), BackspaceHitStroke, labelX, y);
  453. RenderHits(context, BackspaceHitPen, textLine, textLine.GetBackspaceCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
  454. y += VerticalSpacing * 2;
  455. }
  456. if (DistanceStroke != null)
  457. {
  458. y += VerticalSpacing;
  459. var label = RenderLabel(context, nameof(textLine.GetDistanceFromCharacterHit), DistanceStroke, 0, y);
  460. y += label.Height;
  461. for (int i = 0; i < textLine.Length; i++)
  462. {
  463. var hit = new CharacterHit(i);
  464. CharacterHit prevHit = default, nextHit = default;
  465. double leftLabelX = -HorizontalSpacing;
  466. // we want z-order to be previous, next, distance
  467. // but labels need to be ordered next, distance, previous
  468. if (NextHitStroke != null)
  469. {
  470. nextHit = textLine.GetNextCaretCharacterHit(hit);
  471. var nextLabel = RenderLabel(context, $" > {nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}", NextHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
  472. leftLabelX -= nextLabel.WidthIncludingTrailingWhitespace;
  473. }
  474. if (BackspaceHitStroke != null)
  475. {
  476. CharacterHit backHit = textLine.GetBackspaceCaretCharacterHit(hit);
  477. var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(backHit.FirstCharacterIndex, 0));
  478. var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(backHit.FirstCharacterIndex + backHit.TrailingLength, 0));
  479. RenderHorizontalPoint(context, x1, x2, y, BackspaceHitPen, ArrowSize);
  480. }
  481. if (PreviousHitStroke != null)
  482. {
  483. prevHit = textLine.GetPreviousCaretCharacterHit(hit);
  484. var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex, 0));
  485. var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex + prevHit.TrailingLength, 0));
  486. RenderHorizontalPoint(context, x1, x2, y, PreviousHitPen, ArrowSize);
  487. }
  488. if (NextHitStroke != null)
  489. {
  490. var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex, 0));
  491. var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex + nextHit.TrailingLength, 0));
  492. RenderHorizontalPoint(context, x1, x2, y, NextHitPen, ArrowSize);
  493. }
  494. label = RenderLabel(context, $"[{i}]", DistanceStroke, leftLabelX, y, TextAlignment.Right);
  495. leftLabelX -= label.WidthIncludingTrailingWhitespace;
  496. if (PreviousHitStroke != null)
  497. RenderLabel(context, $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength} < ", PreviousHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
  498. double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(i));
  499. RenderHorizontalBar(context, 0, distance, y, DistancePen, ArrowSize);
  500. //RenderLabel(context, distance.ToString("F2"), DistanceStroke, distance + HorizontalSpacing, y, disableCache: true);
  501. y += label.Height;
  502. }
  503. }
  504. }
  505. }
  506. [return: NotNullIfNotNull("brush")]
  507. private FormattedText? RenderLabel(DrawingContext context, string label, IBrush? brush, double x, double y, TextAlignment alignment = TextAlignment.Left, bool disableCache = false)
  508. {
  509. if (brush == null)
  510. return null;
  511. var text = GetOrCreateLabel(label, brush, disableCache);
  512. if (alignment == TextAlignment.Right)
  513. context.DrawText(text, new Point(x - text.WidthIncludingTrailingWhitespace, y - text.Height / 2));
  514. else
  515. context.DrawText(text, new Point(x, y - text.Height / 2));
  516. return text;
  517. }
  518. private void RenderHits(DrawingContext context, IPen hitPen, TextLine textLine, Func<CharacterHit, CharacterHit> nextHit, CharacterHit startingHit, ref double y)
  519. {
  520. CharacterHit lastHit = startingHit;
  521. double lastX = textLine.GetDistanceFromCharacterHit(lastHit);
  522. double lastDirection = 0;
  523. y -= VerticalSpacing; // we always start with adding one below
  524. while (true)
  525. {
  526. CharacterHit hit = nextHit(lastHit);
  527. if (hit == lastHit)
  528. break;
  529. double x = textLine.GetDistanceFromCharacterHit(hit);
  530. double direction = Math.Sign(x - lastX);
  531. if (direction == 0 || lastDirection != direction)
  532. y += VerticalSpacing;
  533. if (direction == 0)
  534. RenderPoint(context, x, y, hitPen, ArrowSize);
  535. else
  536. RenderHorizontalArrow(context, lastX, x, y, hitPen, ArrowSize);
  537. lastX = x;
  538. lastHit = hit;
  539. lastDirection = direction;
  540. }
  541. }
  542. private void RenderPoint(DrawingContext context, double x, double y, IPen pen, double arrowHeight)
  543. {
  544. context.DrawEllipse(pen.Brush, pen, new Point(x, y), ArrowSize / 2, ArrowSize / 2);
  545. }
  546. private void RenderHorizontalPoint(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
  547. {
  548. PathGeometry startCap = new PathGeometry();
  549. PathFigure startFigure = new PathFigure();
  550. startFigure.StartPoint = new Point(xStart, y - size / 2);
  551. startFigure.IsClosed = true;
  552. startFigure.IsFilled = true;
  553. startFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xStart, y + size / 2), SweepDirection = SweepDirection.CounterClockwise });
  554. startCap.Figures!.Add(startFigure);
  555. context.DrawGeometry(pen.Brush, pen, startCap);
  556. PathGeometry endCap = new PathGeometry();
  557. PathFigure endFigure = new PathFigure();
  558. endFigure.StartPoint = new Point(xEnd, y - size / 2);
  559. endFigure.IsClosed = true;
  560. endFigure.IsFilled = false;
  561. endFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xEnd, y + size / 2), SweepDirection = SweepDirection.Clockwise });
  562. endCap.Figures!.Add(endFigure);
  563. context.DrawGeometry(pen.Brush, pen, endCap);
  564. }
  565. private void RenderHorizontalArrow(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
  566. {
  567. context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
  568. context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
  569. if (xEnd >= xStart)
  570. context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
  571. [
  572. new Point(xEnd - size, y - size / 2),
  573. new Point(xEnd - size, y + size/2),
  574. new Point(xEnd, y)
  575. ], isFilled: true));
  576. else
  577. context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
  578. [
  579. new Point(xEnd + size, y - size / 2),
  580. new Point(xEnd + size, y + size/2),
  581. new Point(xEnd, y)
  582. ], isFilled: true));
  583. }
  584. private void RenderHorizontalBar(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
  585. {
  586. context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
  587. context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
  588. context.DrawLine(pen, new Point(xEnd, y - size / 2), new Point(xEnd, y + size / 2)); // end cap
  589. }
  590. private void RenderFontLine(DrawingContext context, double y, double width, IPen pen)
  591. {
  592. context.DrawLine(pen, new Point(0, y), new Point(width, y));
  593. }
  594. }
  595. }