| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using Avalonia.Controls.Documents;
- using Avalonia.Controls.Utils;
- using Avalonia.Input;
- using Avalonia.Input.Platform;
- using Avalonia.Interactivity;
- using Avalonia.Media;
- using Avalonia.Media.TextFormatting;
- using Avalonia.Metadata;
- using Avalonia.Utilities;
- namespace Avalonia.Controls
- {
- /// <summary>
- /// A control that displays a block of formatted text.
- /// </summary>
- public class RichTextBlock : TextBlock, IInlineHost
- {
- public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
- AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
- public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
- AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
- nameof(SelectionStart),
- o => o.SelectionStart,
- (o, v) => o.SelectionStart = v);
- public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
- AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
- nameof(SelectionEnd),
- o => o.SelectionEnd,
- (o, v) => o.SelectionEnd = v);
- public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
- AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
- nameof(SelectedText),
- o => o.SelectedText);
- public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
- AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
- /// <summary>
- /// Defines the <see cref="Inlines"/> property.
- /// </summary>
- public static readonly StyledProperty<InlineCollection?> InlinesProperty =
- AvaloniaProperty.Register<RichTextBlock, InlineCollection?>(
- nameof(Inlines));
- public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
- AvaloniaProperty.RegisterDirect<TextBox, bool>(
- nameof(CanCopy),
- o => o.CanCopy);
- public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
- RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
- nameof(CopyingToClipboard), RoutingStrategies.Bubble);
- private bool _canCopy;
- private int _selectionStart;
- private int _selectionEnd;
- private int _wordSelectionStart = -1;
- static RichTextBlock()
- {
- FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
- AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty);
- }
- public RichTextBlock()
- {
- Inlines = new InlineCollection
- {
- Parent = this,
- InlineHost = this
- };
- }
- /// <summary>
- /// Gets or sets the brush that highlights selected text.
- /// </summary>
- public IBrush? SelectionBrush
- {
- get => GetValue(SelectionBrushProperty);
- set => SetValue(SelectionBrushProperty, value);
- }
- /// <summary>
- /// Gets or sets a character index for the beginning of the current selection.
- /// </summary>
- public int SelectionStart
- {
- get => _selectionStart;
- set
- {
- if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
- {
- RaisePropertyChanged(SelectedTextProperty, "", "");
- }
- }
- }
- /// <summary>
- /// Gets or sets a character index for the end of the current selection.
- /// </summary>
- public int SelectionEnd
- {
- get => _selectionEnd;
- set
- {
- if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
- {
- RaisePropertyChanged(SelectedTextProperty, "", "");
- }
- }
- }
- /// <summary>
- /// Gets the content of the current selection.
- /// </summary>
- public string SelectedText
- {
- get => GetSelection();
- }
- /// <summary>
- /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
- /// </summary>
- public bool IsTextSelectionEnabled
- {
- get => GetValue(IsTextSelectionEnabledProperty);
- set => SetValue(IsTextSelectionEnabledProperty, value);
- }
- /// <summary>
- /// Gets or sets the inlines.
- /// </summary>
- [Content]
- public InlineCollection? Inlines
- {
- get => GetValue(InlinesProperty);
- set => SetValue(InlinesProperty, value);
- }
- /// <summary>
- /// Property for determining if the Copy command can be executed.
- /// </summary>
- public bool CanCopy
- {
- get => _canCopy;
- private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
- }
- public event EventHandler<RoutedEventArgs>? CopyingToClipboard
- {
- add => AddHandler(CopyingToClipboardEvent, value);
- remove => RemoveHandler(CopyingToClipboardEvent, value);
- }
- internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
- /// <summary>
- /// Copies the current selection to the Clipboard.
- /// </summary>
- public async void Copy()
- {
- if (_canCopy || !IsTextSelectionEnabled)
- {
- return;
- }
- var text = GetSelection();
- if (string.IsNullOrEmpty(text))
- {
- return;
- }
- var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
- RaiseEvent(eventArgs);
- if (!eventArgs.Handled)
- {
- await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
- .SetTextAsync(text);
- }
- }
- protected override void RenderTextLayout(DrawingContext context, Point origin)
- {
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
- var selectionBrush = SelectionBrush;
- var selectionEnabled = IsTextSelectionEnabled;
- if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
- {
- var start = Math.Min(selectionStart, selectionEnd);
- var length = Math.Max(selectionStart, selectionEnd) - start;
- var rects = TextLayout.HitTestTextRange(start, length);
- using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
- {
- foreach (var rect in rects)
- {
- context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
- }
- }
- }
- base.RenderTextLayout(context, origin);
- }
- /// <summary>
- /// Select all text in the TextBox
- /// </summary>
- public void SelectAll()
- {
- if (!IsTextSelectionEnabled)
- {
- return;
- }
- var text = Text;
- SelectionStart = 0;
- SelectionEnd = text?.Length ?? 0;
- }
- /// <summary>
- /// Clears the current selection/>
- /// </summary>
- public void ClearSelection()
- {
- if (!IsTextSelectionEnabled)
- {
- return;
- }
- SelectionEnd = SelectionStart;
- }
- protected void AddText(string? text)
- {
- if (string.IsNullOrEmpty(text))
- {
- return;
- }
- if (!HasComplexContent && string.IsNullOrEmpty(_text))
- {
- _text = text;
- }
- else
- {
- if (!string.IsNullOrEmpty(_text))
- {
- Inlines?.Add(_text);
- _text = null;
- }
- Inlines?.Add(text);
- }
- }
- protected override string? GetText()
- {
- return _text ?? Inlines?.Text;
- }
- protected override void SetText(string? text)
- {
- var oldValue = GetText();
-
- AddText(text);
- RaisePropertyChanged(TextProperty, oldValue, text);
- }
- /// <summary>
- /// Creates the <see cref="TextLayout"/> used to render the text.
- /// </summary>
- /// <returns>A <see cref="TextLayout"/> object.</returns>
- protected override TextLayout CreateTextLayout(string? text)
- {
- var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
- var defaultProperties = new GenericTextRunProperties(
- typeface,
- FontSize,
- TextDecorations,
- Foreground);
- var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
- defaultProperties, TextWrapping, LineHeight, 0);
- ITextSource textSource;
- if (HasComplexContent)
- {
- var inlines = Inlines!;
- var textRuns = new List<TextRun>();
- foreach (var inline in inlines)
- {
- inline.BuildTextRun(textRuns);
- }
- textSource = new InlinesTextSource(textRuns);
- }
- else
- {
- textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
- }
- return new TextLayout(
- textSource,
- paragraphProperties,
- TextTrimming,
- _constraint.Width,
- _constraint.Height,
- maxLines: MaxLines,
- lineHeight: LineHeight);
- }
- protected override void OnLostFocus(RoutedEventArgs e)
- {
- base.OnLostFocus(e);
- ClearSelection();
- }
- protected override void OnKeyDown(KeyEventArgs e)
- {
- base.OnKeyDown(e);
- var handled = false;
- var modifiers = e.KeyModifiers;
- var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
- bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
- if (Match(keymap.Copy))
- {
- Copy();
- handled = true;
- }
- e.Handled = handled;
- }
- protected override void OnPointerPressed(PointerPressedEventArgs e)
- {
- base.OnPointerPressed(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
- var text = Text;
- var clickInfo = e.GetCurrentPoint(this);
- if (text != null && clickInfo.Properties.IsLeftButtonPressed)
- {
- var padding = Padding;
- var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
- var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
- var oldIndex = SelectionStart;
- var hit = TextLayout.HitTestPoint(point);
- var index = hit.TextPosition;
- switch (e.ClickCount)
- {
- case 1:
- if (clickToSelect)
- {
- if (_wordSelectionStart >= 0)
- {
- var previousWord = StringUtils.PreviousWord(text, index);
- if (index > _wordSelectionStart)
- {
- SelectionEnd = StringUtils.NextWord(text, index);
- }
- if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
- {
- SelectionStart = previousWord;
- }
- }
- else
- {
- SelectionStart = Math.Min(oldIndex, index);
- SelectionEnd = Math.Max(oldIndex, index);
- }
- }
- else
- {
- if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
- {
- SelectionStart = SelectionEnd = index;
- _wordSelectionStart = -1;
- }
- }
- break;
- case 2:
- if (!StringUtils.IsStartOfWord(text, index))
- {
- SelectionStart = StringUtils.PreviousWord(text, index);
- }
- _wordSelectionStart = SelectionStart;
- SelectionEnd = StringUtils.NextWord(text, index);
- break;
- case 3:
- _wordSelectionStart = -1;
- SelectAll();
- break;
- }
- }
- e.Pointer.Capture(this);
- e.Handled = true;
- }
- protected override void OnPointerMoved(PointerEventArgs e)
- {
- base.OnPointerMoved(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
- // selection should not change during pointer move if the user right clicks
- if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
- {
- var text = Text;
- var padding = Padding;
- var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
- point = new Point(
- MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)),
- MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0)));
- var hit = TextLayout.HitTestPoint(point);
- var textPosition = hit.TextPosition;
- if (text != null && _wordSelectionStart >= 0)
- {
- var distance = textPosition - _wordSelectionStart;
- if (distance <= 0)
- {
- SelectionStart = StringUtils.PreviousWord(text, textPosition);
- }
- if (distance >= 0)
- {
- if (SelectionStart != _wordSelectionStart)
- {
- SelectionStart = _wordSelectionStart;
- }
- SelectionEnd = StringUtils.NextWord(text, textPosition);
- }
- }
- else
- {
- SelectionEnd = textPosition;
- }
- }
- }
- protected override void OnPointerReleased(PointerReleasedEventArgs e)
- {
- base.OnPointerReleased(e);
- if (!IsTextSelectionEnabled)
- {
- return;
- }
- if (e.Pointer.Captured != this)
- {
- return;
- }
- if (e.InitialPressMouseButton == MouseButton.Right)
- {
- var padding = Padding;
- var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
- var hit = TextLayout.HitTestPoint(point);
- var caretIndex = hit.TextPosition;
- // see if mouse clicked inside current selection
- // if it did not, we change the selection to where the user clicked
- var firstSelection = Math.Min(SelectionStart, SelectionEnd);
- var lastSelection = Math.Max(SelectionStart, SelectionEnd);
- var didClickInSelection = SelectionStart != SelectionEnd &&
- caretIndex >= firstSelection && caretIndex <= lastSelection;
- if (!didClickInSelection)
- {
- SelectionStart = SelectionEnd = caretIndex;
- }
- }
- e.Pointer.Capture(null);
- }
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
- switch (change.Property.Name)
- {
- case nameof(Inlines):
- {
- OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
- InvalidateTextLayout();
- break;
- }
- }
- }
- private string GetSelection()
- {
- if (!IsTextSelectionEnabled)
- {
- return "";
- }
- var text = GetText();
- if (string.IsNullOrEmpty(text))
- {
- return "";
- }
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
- var start = Math.Min(selectionStart, selectionEnd);
- var end = Math.Max(selectionStart, selectionEnd);
- if (start == end || text.Length < end)
- {
- return "";
- }
- var length = Math.Max(0, end - start);
- var selectedText = text.Substring(start, length);
- return selectedText;
- }
- private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
- {
- if (oldValue is not null)
- {
- oldValue.Parent = null;
- oldValue.InlineHost = null;
- oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
- }
- if (newValue is not null)
- {
- newValue.Parent = this;
- newValue.InlineHost = this;
- newValue.Invalidated += (s, e) => InvalidateTextLayout();
- }
- }
- void IInlineHost.AddVisualChild(IControl child)
- {
- if (child.VisualParent == null)
- {
- VisualChildren.Add(child);
- }
- }
- void IInlineHost.Invalidate()
- {
- InvalidateTextLayout();
- }
- private readonly struct InlinesTextSource : ITextSource
- {
- private readonly IReadOnlyList<TextRun> _textRuns;
- public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
- {
- _textRuns = textRuns;
- }
- public TextRun? GetTextRun(int textSourceIndex)
- {
- var currentPosition = 0;
- foreach (var textRun in _textRuns)
- {
- if (textRun.TextSourceLength == 0)
- {
- continue;
- }
- if (currentPosition >= textSourceIndex)
- {
- return textRun;
- }
- currentPosition += textRun.TextSourceLength;
- }
- return null;
- }
- }
- }
- }
|