InteractiveLineControl.cs 28 KB

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