RichTextBlock.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Avalonia.Controls.Documents;
  5. using Avalonia.Controls.Utils;
  6. using Avalonia.Input;
  7. using Avalonia.Input.Platform;
  8. using Avalonia.Interactivity;
  9. using Avalonia.Media;
  10. using Avalonia.Media.TextFormatting;
  11. using Avalonia.Metadata;
  12. using Avalonia.Utilities;
  13. namespace Avalonia.Controls
  14. {
  15. /// <summary>
  16. /// A control that displays a block of formatted text.
  17. /// </summary>
  18. public class RichTextBlock : TextBlock, IInlineHost
  19. {
  20. public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
  21. AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
  22. public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
  23. AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
  24. nameof(SelectionStart),
  25. o => o.SelectionStart,
  26. (o, v) => o.SelectionStart = v);
  27. public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
  28. AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
  29. nameof(SelectionEnd),
  30. o => o.SelectionEnd,
  31. (o, v) => o.SelectionEnd = v);
  32. public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
  33. AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
  34. nameof(SelectedText),
  35. o => o.SelectedText);
  36. public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
  37. AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
  38. /// <summary>
  39. /// Defines the <see cref="Inlines"/> property.
  40. /// </summary>
  41. public static readonly StyledProperty<InlineCollection?> InlinesProperty =
  42. AvaloniaProperty.Register<RichTextBlock, InlineCollection?>(
  43. nameof(Inlines));
  44. public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
  45. AvaloniaProperty.RegisterDirect<TextBox, bool>(
  46. nameof(CanCopy),
  47. o => o.CanCopy);
  48. public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
  49. RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
  50. nameof(CopyingToClipboard), RoutingStrategies.Bubble);
  51. private bool _canCopy;
  52. private int _selectionStart;
  53. private int _selectionEnd;
  54. private int _wordSelectionStart = -1;
  55. static RichTextBlock()
  56. {
  57. FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
  58. AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
  59. }
  60. public RichTextBlock()
  61. {
  62. Inlines = new InlineCollection
  63. {
  64. Parent = this,
  65. InlineHost = this
  66. };
  67. }
  68. /// <summary>
  69. /// Gets or sets the brush that highlights selected text.
  70. /// </summary>
  71. public IBrush? SelectionBrush
  72. {
  73. get => GetValue(SelectionBrushProperty);
  74. set => SetValue(SelectionBrushProperty, value);
  75. }
  76. /// <summary>
  77. /// Gets or sets a character index for the beginning of the current selection.
  78. /// </summary>
  79. public int SelectionStart
  80. {
  81. get => _selectionStart;
  82. set
  83. {
  84. if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
  85. {
  86. RaisePropertyChanged(SelectedTextProperty, "", "");
  87. }
  88. }
  89. }
  90. /// <summary>
  91. /// Gets or sets a character index for the end of the current selection.
  92. /// </summary>
  93. public int SelectionEnd
  94. {
  95. get => _selectionEnd;
  96. set
  97. {
  98. if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
  99. {
  100. RaisePropertyChanged(SelectedTextProperty, "", "");
  101. }
  102. }
  103. }
  104. /// <summary>
  105. /// Gets the content of the current selection.
  106. /// </summary>
  107. public string SelectedText
  108. {
  109. get => GetSelection();
  110. }
  111. /// <summary>
  112. /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
  113. /// </summary>
  114. public bool IsTextSelectionEnabled
  115. {
  116. get => GetValue(IsTextSelectionEnabledProperty);
  117. set => SetValue(IsTextSelectionEnabledProperty, value);
  118. }
  119. /// <summary>
  120. /// Gets or sets the inlines.
  121. /// </summary>
  122. [Content]
  123. public InlineCollection? Inlines
  124. {
  125. get => GetValue(InlinesProperty);
  126. set => SetValue(InlinesProperty, value);
  127. }
  128. /// <summary>
  129. /// Property for determining if the Copy command can be executed.
  130. /// </summary>
  131. public bool CanCopy
  132. {
  133. get => _canCopy;
  134. private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
  135. }
  136. public event EventHandler<RoutedEventArgs>? CopyingToClipboard
  137. {
  138. add => AddHandler(CopyingToClipboardEvent, value);
  139. remove => RemoveHandler(CopyingToClipboardEvent, value);
  140. }
  141. internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
  142. /// <summary>
  143. /// Copies the current selection to the Clipboard.
  144. /// </summary>
  145. public async void Copy()
  146. {
  147. if (_canCopy || !IsTextSelectionEnabled)
  148. {
  149. return;
  150. }
  151. var text = GetSelection();
  152. if (string.IsNullOrEmpty(text))
  153. {
  154. return;
  155. }
  156. var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
  157. RaiseEvent(eventArgs);
  158. if (!eventArgs.Handled)
  159. {
  160. await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
  161. .SetTextAsync(text);
  162. }
  163. }
  164. protected override void RenderTextLayout(DrawingContext context, Point origin)
  165. {
  166. var selectionStart = SelectionStart;
  167. var selectionEnd = SelectionEnd;
  168. var selectionBrush = SelectionBrush;
  169. var selectionEnabled = IsTextSelectionEnabled;
  170. if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
  171. {
  172. var start = Math.Min(selectionStart, selectionEnd);
  173. var length = Math.Max(selectionStart, selectionEnd) - start;
  174. var rects = TextLayout.HitTestTextRange(start, length);
  175. using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
  176. {
  177. foreach (var rect in rects)
  178. {
  179. context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
  180. }
  181. }
  182. }
  183. base.RenderTextLayout(context, origin);
  184. }
  185. /// <summary>
  186. /// Select all text in the TextBox
  187. /// </summary>
  188. public void SelectAll()
  189. {
  190. if (!IsTextSelectionEnabled)
  191. {
  192. return;
  193. }
  194. var text = Text;
  195. SelectionStart = 0;
  196. SelectionEnd = text?.Length ?? 0;
  197. }
  198. /// <summary>
  199. /// Clears the current selection/>
  200. /// </summary>
  201. public void ClearSelection()
  202. {
  203. if (!IsTextSelectionEnabled)
  204. {
  205. return;
  206. }
  207. SelectionEnd = SelectionStart;
  208. }
  209. protected void AddText(string? text)
  210. {
  211. if (string.IsNullOrEmpty(text))
  212. {
  213. return;
  214. }
  215. if (!HasComplexContent && string.IsNullOrEmpty(_text))
  216. {
  217. _text = text;
  218. }
  219. else
  220. {
  221. if (!string.IsNullOrEmpty(_text))
  222. {
  223. Inlines?.Add(_text);
  224. _text = null;
  225. }
  226. Inlines?.Add(text);
  227. }
  228. }
  229. protected override string? GetText()
  230. {
  231. return _text ?? Inlines?.Text;
  232. }
  233. protected override void SetText(string? text)
  234. {
  235. var oldValue = GetText();
  236. AddText(text);
  237. RaisePropertyChanged(TextProperty, oldValue, text);
  238. }
  239. /// <summary>
  240. /// Creates the <see cref="TextLayout"/> used to render the text.
  241. /// </summary>
  242. /// <returns>A <see cref="TextLayout"/> object.</returns>
  243. protected override TextLayout CreateTextLayout(string? text)
  244. {
  245. var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
  246. var defaultProperties = new GenericTextRunProperties(
  247. typeface,
  248. FontSize,
  249. TextDecorations,
  250. Foreground);
  251. var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
  252. defaultProperties, TextWrapping, LineHeight, 0);
  253. ITextSource textSource;
  254. if (HasComplexContent)
  255. {
  256. var inlines = Inlines!;
  257. var textRuns = new List<TextRun>();
  258. foreach (var inline in inlines)
  259. {
  260. inline.BuildTextRun(textRuns);
  261. }
  262. textSource = new InlinesTextSource(textRuns);
  263. }
  264. else
  265. {
  266. textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
  267. }
  268. return new TextLayout(
  269. textSource,
  270. paragraphProperties,
  271. TextTrimming,
  272. _constraint.Width,
  273. _constraint.Height,
  274. maxLines: MaxLines,
  275. lineHeight: LineHeight);
  276. }
  277. protected override void OnLostFocus(RoutedEventArgs e)
  278. {
  279. base.OnLostFocus(e);
  280. ClearSelection();
  281. }
  282. protected override void OnKeyDown(KeyEventArgs e)
  283. {
  284. base.OnKeyDown(e);
  285. var handled = false;
  286. var modifiers = e.KeyModifiers;
  287. var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
  288. bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
  289. if (Match(keymap.Copy))
  290. {
  291. Copy();
  292. handled = true;
  293. }
  294. e.Handled = handled;
  295. }
  296. protected override void OnPointerPressed(PointerPressedEventArgs e)
  297. {
  298. base.OnPointerPressed(e);
  299. if (!IsTextSelectionEnabled)
  300. {
  301. return;
  302. }
  303. var text = Text;
  304. var clickInfo = e.GetCurrentPoint(this);
  305. if (text != null && clickInfo.Properties.IsLeftButtonPressed)
  306. {
  307. var padding = Padding;
  308. var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
  309. var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
  310. var oldIndex = SelectionStart;
  311. var hit = TextLayout.HitTestPoint(point);
  312. var index = hit.TextPosition;
  313. switch (e.ClickCount)
  314. {
  315. case 1:
  316. if (clickToSelect)
  317. {
  318. if (_wordSelectionStart >= 0)
  319. {
  320. var previousWord = StringUtils.PreviousWord(text, index);
  321. if (index > _wordSelectionStart)
  322. {
  323. SelectionEnd = StringUtils.NextWord(text, index);
  324. }
  325. if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
  326. {
  327. SelectionStart = previousWord;
  328. }
  329. }
  330. else
  331. {
  332. SelectionStart = Math.Min(oldIndex, index);
  333. SelectionEnd = Math.Max(oldIndex, index);
  334. }
  335. }
  336. else
  337. {
  338. if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
  339. {
  340. SelectionStart = SelectionEnd = index;
  341. _wordSelectionStart = -1;
  342. }
  343. }
  344. break;
  345. case 2:
  346. if (!StringUtils.IsStartOfWord(text, index))
  347. {
  348. SelectionStart = StringUtils.PreviousWord(text, index);
  349. }
  350. _wordSelectionStart = SelectionStart;
  351. SelectionEnd = StringUtils.NextWord(text, index);
  352. break;
  353. case 3:
  354. _wordSelectionStart = -1;
  355. SelectAll();
  356. break;
  357. }
  358. }
  359. e.Pointer.Capture(this);
  360. e.Handled = true;
  361. }
  362. protected override void OnPointerMoved(PointerEventArgs e)
  363. {
  364. base.OnPointerMoved(e);
  365. if (!IsTextSelectionEnabled)
  366. {
  367. return;
  368. }
  369. // selection should not change during pointer move if the user right clicks
  370. if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
  371. {
  372. var text = Text;
  373. var padding = Padding;
  374. var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
  375. point = new Point(
  376. MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)),
  377. MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0)));
  378. var hit = TextLayout.HitTestPoint(point);
  379. var textPosition = hit.TextPosition;
  380. if (text != null && _wordSelectionStart >= 0)
  381. {
  382. var distance = textPosition - _wordSelectionStart;
  383. if (distance <= 0)
  384. {
  385. SelectionStart = StringUtils.PreviousWord(text, textPosition);
  386. }
  387. if (distance >= 0)
  388. {
  389. if (SelectionStart != _wordSelectionStart)
  390. {
  391. SelectionStart = _wordSelectionStart;
  392. }
  393. SelectionEnd = StringUtils.NextWord(text, textPosition);
  394. }
  395. }
  396. else
  397. {
  398. SelectionEnd = textPosition;
  399. }
  400. }
  401. }
  402. protected override void OnPointerReleased(PointerReleasedEventArgs e)
  403. {
  404. base.OnPointerReleased(e);
  405. if (!IsTextSelectionEnabled)
  406. {
  407. return;
  408. }
  409. if (e.Pointer.Captured != this)
  410. {
  411. return;
  412. }
  413. if (e.InitialPressMouseButton == MouseButton.Right)
  414. {
  415. var padding = Padding;
  416. var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
  417. var hit = TextLayout.HitTestPoint(point);
  418. var caretIndex = hit.TextPosition;
  419. // see if mouse clicked inside current selection
  420. // if it did not, we change the selection to where the user clicked
  421. var firstSelection = Math.Min(SelectionStart, SelectionEnd);
  422. var lastSelection = Math.Max(SelectionStart, SelectionEnd);
  423. var didClickInSelection = SelectionStart != SelectionEnd &&
  424. caretIndex >= firstSelection && caretIndex <= lastSelection;
  425. if (!didClickInSelection)
  426. {
  427. SelectionStart = SelectionEnd = caretIndex;
  428. }
  429. }
  430. e.Pointer.Capture(null);
  431. }
  432. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
  433. {
  434. base.OnPropertyChanged(change);
  435. switch (change.Property.Name)
  436. {
  437. case nameof(Inlines):
  438. {
  439. OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
  440. InvalidateTextLayout();
  441. break;
  442. }
  443. }
  444. }
  445. private string GetSelection()
  446. {
  447. if (!IsTextSelectionEnabled)
  448. {
  449. return "";
  450. }
  451. var text = GetText();
  452. if (string.IsNullOrEmpty(text))
  453. {
  454. return "";
  455. }
  456. var selectionStart = SelectionStart;
  457. var selectionEnd = SelectionEnd;
  458. var start = Math.Min(selectionStart, selectionEnd);
  459. var end = Math.Max(selectionStart, selectionEnd);
  460. if (start == end || text.Length < end)
  461. {
  462. return "";
  463. }
  464. var length = Math.Max(0, end - start);
  465. var selectedText = text.Substring(start, length);
  466. return selectedText;
  467. }
  468. private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
  469. {
  470. if (oldValue is not null)
  471. {
  472. oldValue.Parent = null;
  473. oldValue.InlineHost = null;
  474. oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
  475. }
  476. if (newValue is not null)
  477. {
  478. newValue.Parent = this;
  479. newValue.InlineHost = this;
  480. newValue.Invalidated += (s, e) => InvalidateTextLayout();
  481. }
  482. }
  483. void IInlineHost.AddVisualChild(IControl child)
  484. {
  485. if (child.VisualParent == null)
  486. {
  487. VisualChildren.Add(child);
  488. }
  489. }
  490. void IInlineHost.Invalidate()
  491. {
  492. InvalidateTextLayout();
  493. }
  494. private readonly struct InlinesTextSource : ITextSource
  495. {
  496. private readonly IReadOnlyList<TextRun> _textRuns;
  497. public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
  498. {
  499. _textRuns = textRuns;
  500. }
  501. public TextRun? GetTextRun(int textSourceIndex)
  502. {
  503. var currentPosition = 0;
  504. foreach (var textRun in _textRuns)
  505. {
  506. if (textRun.TextSourceLength == 0)
  507. {
  508. continue;
  509. }
  510. if (currentPosition >= textSourceIndex)
  511. {
  512. return textRun;
  513. }
  514. currentPosition += textRun.TextSourceLength;
  515. }
  516. return null;
  517. }
  518. }
  519. }
  520. }