csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1062 lines
34 KiB
1062 lines
34 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using Avalonia.Controls.Documents;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Media.Immutable;
|
|
using Avalonia.Media.TextFormatting;
|
|
using Avalonia.Metadata;
|
|
using Avalonia.Threading;
|
|
using Avalonia.Utilities;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Controls.Presenters
|
|
{
|
|
public class TextPresenter : Control
|
|
{
|
|
public static readonly StyledProperty<bool> ShowSelectionHighlightProperty =
|
|
AvaloniaProperty.Register<TextPresenter, bool>(nameof(ShowSelectionHighlight), defaultValue: true);
|
|
|
|
public static readonly StyledProperty<int> CaretIndexProperty =
|
|
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
|
|
|
|
public static readonly StyledProperty<bool> RevealPasswordProperty =
|
|
AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword));
|
|
|
|
public static readonly StyledProperty<char> PasswordCharProperty =
|
|
AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar));
|
|
|
|
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
|
|
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(SelectionBrush));
|
|
|
|
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
|
|
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(SelectionForegroundBrush));
|
|
|
|
public static readonly StyledProperty<IBrush?> CaretBrushProperty =
|
|
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
|
|
|
|
public static readonly StyledProperty<TimeSpan> CaretBlinkIntervalProperty =
|
|
TextBox.CaretBlinkIntervalProperty.AddOwner<TextPresenter>();
|
|
|
|
public static readonly StyledProperty<int> SelectionStartProperty =
|
|
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
|
|
|
|
public static readonly StyledProperty<int> SelectionEndProperty =
|
|
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="Text"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<string?> TextProperty =
|
|
TextBlock.TextProperty.AddOwner<TextPresenter>(new(string.Empty));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="PreeditText"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<string?> PreeditTextProperty =
|
|
AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="PreeditText"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<int?> PreeditTextCursorPositionProperty =
|
|
AvaloniaProperty.Register<TextPresenter, int?>(nameof(PreeditTextCursorPosition));
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="TextAlignment"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
|
|
TextBlock.TextAlignmentProperty.AddOwner<TextPresenter>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="TextWrapping"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
|
|
TextBlock.TextWrappingProperty.AddOwner<TextPresenter>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="LineHeight"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<double> LineHeightProperty =
|
|
TextBlock.LineHeightProperty.AddOwner<TextPresenter>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="LetterSpacing"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<double> LetterSpacingProperty =
|
|
TextBlock.LetterSpacingProperty.AddOwner<TextPresenter>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="Background"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<IBrush?> BackgroundProperty =
|
|
Border.BackgroundProperty.AddOwner<TextPresenter>();
|
|
|
|
private DispatcherTimer? _caretTimer;
|
|
private bool _caretBlink;
|
|
private TextLayout? _textLayout;
|
|
private Size _constraint;
|
|
|
|
private CharacterHit _lastCharacterHit;
|
|
private Rect _caretBounds;
|
|
private Point _navigationPosition;
|
|
private Point? _previousOffset;
|
|
private TextSelectorLayer? _layer;
|
|
|
|
static TextPresenter()
|
|
{
|
|
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty, ShowSelectionHighlightProperty);
|
|
}
|
|
|
|
public TextPresenter() { }
|
|
|
|
public event EventHandler? CaretBoundsChanged;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a brush used to paint the control's background.
|
|
/// </summary>
|
|
public IBrush? Background
|
|
{
|
|
get => GetValue(BackgroundProperty);
|
|
set => SetValue(BackgroundProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value that determines whether the TextPresenter shows a selection highlight.
|
|
/// </summary>
|
|
public bool ShowSelectionHighlight
|
|
{
|
|
get => GetValue(ShowSelectionHighlightProperty);
|
|
set => SetValue(ShowSelectionHighlightProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the text.
|
|
/// </summary>
|
|
[Content]
|
|
public string? Text
|
|
{
|
|
get => GetValue(TextProperty);
|
|
set => SetValue(TextProperty, value);
|
|
}
|
|
|
|
public string? PreeditText
|
|
{
|
|
get => GetValue(PreeditTextProperty);
|
|
set => SetValue(PreeditTextProperty, value);
|
|
}
|
|
|
|
public int? PreeditTextCursorPosition
|
|
{
|
|
get => GetValue(PreeditTextCursorPositionProperty);
|
|
set => SetValue(PreeditTextCursorPositionProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font family.
|
|
/// </summary>
|
|
public FontFamily FontFamily
|
|
{
|
|
get => TextElement.GetFontFamily(this);
|
|
set => TextElement.SetFontFamily(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font family.
|
|
/// </summary>
|
|
public FontFeatureCollection? FontFeatures
|
|
{
|
|
get => TextElement.GetFontFeatures(this);
|
|
set => TextElement.SetFontFeatures(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font size.
|
|
/// </summary>
|
|
public double FontSize
|
|
{
|
|
get => TextElement.GetFontSize(this);
|
|
set => TextElement.SetFontSize(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font style.
|
|
/// </summary>
|
|
public FontStyle FontStyle
|
|
{
|
|
get => TextElement.GetFontStyle(this);
|
|
set => TextElement.SetFontStyle(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font weight.
|
|
/// </summary>
|
|
public FontWeight FontWeight
|
|
{
|
|
get => TextElement.GetFontWeight(this);
|
|
set => TextElement.SetFontWeight(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font stretch.
|
|
/// </summary>
|
|
public FontStretch FontStretch
|
|
{
|
|
get => TextElement.GetFontStretch(this);
|
|
set => TextElement.SetFontStretch(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a brush used to paint the text.
|
|
/// </summary>
|
|
public IBrush? Foreground
|
|
{
|
|
get => TextElement.GetForeground(this);
|
|
set => TextElement.SetForeground(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the control's text wrapping mode.
|
|
/// </summary>
|
|
public TextWrapping TextWrapping
|
|
{
|
|
get => GetValue(TextWrappingProperty);
|
|
set => SetValue(TextWrappingProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the line height. By default, this is set to <see cref="double.NaN"/>, which determines the appropriate height automatically.
|
|
/// </summary>
|
|
public double LineHeight
|
|
{
|
|
get => GetValue(LineHeightProperty);
|
|
set => SetValue(LineHeightProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the letter spacing.
|
|
/// </summary>
|
|
public double LetterSpacing
|
|
{
|
|
get => GetValue(LetterSpacingProperty);
|
|
set => SetValue(LetterSpacingProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the text alignment.
|
|
/// </summary>
|
|
public TextAlignment TextAlignment
|
|
{
|
|
get => GetValue(TextAlignmentProperty);
|
|
set => SetValue(TextAlignmentProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="TextLayout"/> used to render the text.
|
|
/// </summary>
|
|
public TextLayout TextLayout
|
|
{
|
|
get
|
|
{
|
|
if (_textLayout != null)
|
|
{
|
|
return _textLayout;
|
|
}
|
|
|
|
_textLayout = CreateTextLayout();
|
|
|
|
UpdateCaret(_lastCharacterHit, false);
|
|
|
|
return _textLayout;
|
|
}
|
|
}
|
|
|
|
public int CaretIndex
|
|
{
|
|
get => GetValue(CaretIndexProperty);
|
|
set => SetValue(CaretIndexProperty, value);
|
|
}
|
|
|
|
public char PasswordChar
|
|
{
|
|
get => GetValue(PasswordCharProperty);
|
|
set => SetValue(PasswordCharProperty, value);
|
|
}
|
|
|
|
public bool RevealPassword
|
|
{
|
|
get => GetValue(RevealPasswordProperty);
|
|
set => SetValue(RevealPasswordProperty, value);
|
|
}
|
|
|
|
public IBrush? SelectionBrush
|
|
{
|
|
get => GetValue(SelectionBrushProperty);
|
|
set => SetValue(SelectionBrushProperty, value);
|
|
}
|
|
|
|
public IBrush? SelectionForegroundBrush
|
|
{
|
|
get => GetValue(SelectionForegroundBrushProperty);
|
|
set => SetValue(SelectionForegroundBrushProperty, value);
|
|
}
|
|
|
|
public IBrush? CaretBrush
|
|
{
|
|
get => GetValue(CaretBrushProperty);
|
|
set => SetValue(CaretBrushProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the caret blink rate
|
|
/// </summary>
|
|
public TimeSpan CaretBlinkInterval
|
|
{
|
|
get => GetValue(CaretBlinkIntervalProperty);
|
|
set => SetValue(CaretBlinkIntervalProperty, value);
|
|
}
|
|
|
|
public int SelectionStart
|
|
{
|
|
get => GetValue(SelectionStartProperty);
|
|
set => SetValue(SelectionStartProperty, value);
|
|
}
|
|
|
|
public int SelectionEnd
|
|
{
|
|
get => GetValue(SelectionEndProperty);
|
|
set => SetValue(SelectionEndProperty, value);
|
|
}
|
|
|
|
protected override bool BypassFlowDirectionPolicies => true;
|
|
|
|
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
|
|
|
|
/// <summary>
|
|
/// Creates the <see cref="TextLayout"/> used to render the text.
|
|
/// </summary>
|
|
/// <param name="constraint">The constraint of the text.</param>
|
|
/// <param name="text">The text to format.</param>
|
|
/// <param name="typeface"></param>
|
|
/// <param name="textStyleOverrides"></param>
|
|
/// <returns>A <see cref="TextLayout"/> object.</returns>
|
|
private TextLayout CreateTextLayoutInternal(Size constraint, string? text, Typeface typeface,
|
|
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides)
|
|
{
|
|
var foreground = Foreground;
|
|
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
|
|
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
|
|
|
|
var textLayout = new TextLayout(text, typeface, FontFeatures, FontSize, foreground, TextAlignment,
|
|
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
|
|
flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
|
|
|
|
return textLayout;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the <see cref="TextPresenter"/> to a drawing context.
|
|
/// </summary>
|
|
/// <param name="context">The drawing context.</param>
|
|
private void RenderInternal(DrawingContext context)
|
|
{
|
|
var background = Background;
|
|
|
|
if (background != null)
|
|
{
|
|
context.FillRectangle(background, new Rect(Bounds.Size));
|
|
}
|
|
|
|
var top = 0d;
|
|
var left = 0.0;
|
|
|
|
var textHeight = TextLayout.Height;
|
|
|
|
if (Bounds.Height < textHeight)
|
|
{
|
|
switch (VerticalAlignment)
|
|
{
|
|
case VerticalAlignment.Center:
|
|
top += (Bounds.Height - textHeight) / 2;
|
|
break;
|
|
|
|
case VerticalAlignment.Bottom:
|
|
top += (Bounds.Height - textHeight);
|
|
break;
|
|
}
|
|
}
|
|
|
|
TextLayout.Draw(context, new Point(left, top));
|
|
}
|
|
|
|
public sealed override void Render(DrawingContext context)
|
|
{
|
|
var selectionStart = SelectionStart;
|
|
var selectionEnd = SelectionEnd;
|
|
var selectionBrush = SelectionBrush;
|
|
|
|
if (ShowSelectionHighlight && selectionStart != selectionEnd && selectionBrush != null)
|
|
{
|
|
var start = Math.Min(selectionStart, selectionEnd);
|
|
var length = Math.Max(selectionStart, selectionEnd) - start;
|
|
|
|
var rects = TextLayout.HitTestTextRange(start, length);
|
|
|
|
foreach (var rect in rects)
|
|
{
|
|
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
|
|
}
|
|
}
|
|
|
|
if(VisualRoot is Visual root)
|
|
{
|
|
var offset = this.TranslatePoint(Bounds.Position, root);
|
|
|
|
if(_previousOffset != offset)
|
|
{
|
|
_previousOffset = offset;
|
|
}
|
|
}
|
|
|
|
RenderInternal(context);
|
|
|
|
if ((selectionStart != selectionEnd || !_caretBlink))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var caretBrush = CaretBrush?.ToImmutable();
|
|
|
|
if (caretBrush is null)
|
|
{
|
|
var backgroundColor = (Background as ISolidColorBrush)?.Color;
|
|
|
|
if (backgroundColor.HasValue)
|
|
{
|
|
var red = (byte)~(backgroundColor.Value.R);
|
|
var green = (byte)~(backgroundColor.Value.G);
|
|
var blue = (byte)~(backgroundColor.Value.B);
|
|
|
|
caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
|
|
}
|
|
else
|
|
{
|
|
caretBrush = Brushes.Black;
|
|
}
|
|
}
|
|
|
|
var (p1, p2) = GetCaretPoints();
|
|
|
|
context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
|
|
}
|
|
|
|
internal (Point, Point) GetCaretPoints()
|
|
{
|
|
var x = Math.Floor(_caretBounds.X) + 0.5;
|
|
var y = Math.Floor(_caretBounds.Y) + 0.5;
|
|
var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
|
|
|
|
var caretIndex = _lastCharacterHit.FirstCharacterIndex + _lastCharacterHit.TrailingLength;
|
|
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0);
|
|
var textLine = TextLayout.TextLines[lineIndex];
|
|
|
|
if (_caretBounds.X > 0 && _caretBounds.X >= textLine.WidthIncludingTrailingWhitespace)
|
|
{
|
|
x -= 1;
|
|
}
|
|
|
|
return (new Point(x, y), new Point(x, b));
|
|
}
|
|
|
|
public void ShowCaret()
|
|
{
|
|
EnsureCaretTimer();
|
|
EnsureTextSelectionLayer();
|
|
_caretBlink = true;
|
|
_caretTimer?.Start();
|
|
InvalidateVisual();
|
|
}
|
|
|
|
public void HideCaret()
|
|
{
|
|
_caretBlink = false;
|
|
RemoveTextSelectionCanvas();
|
|
_caretTimer?.Stop();
|
|
InvalidateTextLayout();
|
|
}
|
|
|
|
internal void CaretChanged()
|
|
{
|
|
if (this.GetVisualParent() == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureCaretTimer();
|
|
|
|
if (_caretTimer?.IsEnabled ?? false)
|
|
{
|
|
_caretBlink = true;
|
|
_caretTimer?.Stop();
|
|
_caretTimer?.Start();
|
|
InvalidateVisual();
|
|
}
|
|
else
|
|
{
|
|
_caretTimer?.Start();
|
|
InvalidateVisual();
|
|
_caretTimer?.Stop();
|
|
}
|
|
|
|
if (IsMeasureValid)
|
|
{
|
|
this.BringIntoView(_caretBounds);
|
|
}
|
|
else
|
|
{
|
|
// The measure is currently invalid so there's no point trying to bring the
|
|
// current char into view until a measure has been carried out as the scroll
|
|
// viewer extents may not be up-to-date.
|
|
Dispatcher.UIThread.Post(
|
|
() =>
|
|
{
|
|
this.BringIntoView(_caretBounds);
|
|
},
|
|
DispatcherPriority.AfterRender);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the <see cref="TextLayout"/> used to render the text.
|
|
/// </summary>
|
|
/// <returns>A <see cref="TextLayout"/> object.</returns>
|
|
protected virtual TextLayout CreateTextLayout()
|
|
{
|
|
TextLayout result;
|
|
|
|
var caretIndex = CaretIndex;
|
|
var preeditText = PreeditText;
|
|
var text = GetCombinedText(Text, caretIndex, preeditText);
|
|
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
|
|
var selectionStart = SelectionStart;
|
|
var selectionEnd = SelectionEnd;
|
|
var start = Math.Min(selectionStart, selectionEnd);
|
|
var length = Math.Max(selectionStart, selectionEnd) - start;
|
|
|
|
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
|
|
|
|
var foreground = Foreground;
|
|
|
|
if (!string.IsNullOrEmpty(preeditText))
|
|
{
|
|
var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length,
|
|
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
|
|
foregroundBrush: foreground,
|
|
textDecorations: TextDecorations.Underline));
|
|
|
|
textStyleOverrides = new[]
|
|
{
|
|
preeditHighlight
|
|
};
|
|
}
|
|
else
|
|
{
|
|
if (ShowSelectionHighlight && length > 0 && SelectionForegroundBrush != null)
|
|
{
|
|
textStyleOverrides = new[]
|
|
{
|
|
new ValueSpan<TextRunProperties>(start, length,
|
|
new GenericTextRunProperties(typeface, FontFeatures, FontSize,
|
|
foregroundBrush: SelectionForegroundBrush))
|
|
};
|
|
}
|
|
}
|
|
|
|
if (PasswordChar != default(char) && !RevealPassword)
|
|
{
|
|
result = CreateTextLayoutInternal(_constraint, new string(PasswordChar, text?.Length ?? 0), typeface,
|
|
textStyleOverrides);
|
|
}
|
|
else
|
|
{
|
|
result = CreateTextLayoutInternal(_constraint, text, typeface, textStyleOverrides);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string? GetCombinedText(string? text, int caretIndex, string? preeditText)
|
|
{
|
|
if (string.IsNullOrEmpty(preeditText))
|
|
{
|
|
return text;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return preeditText;
|
|
}
|
|
|
|
var sb = StringBuilderCache.Acquire(text.Length + preeditText.Length);
|
|
|
|
sb.Append(text.Substring(0, caretIndex));
|
|
sb.Insert(caretIndex, preeditText);
|
|
sb.Append(text.Substring(caretIndex));
|
|
|
|
return StringBuilderCache.GetStringAndRelease(sb);
|
|
}
|
|
|
|
protected virtual void InvalidateTextLayout()
|
|
{
|
|
_textLayout?.Dispose();
|
|
_textLayout = null;
|
|
|
|
InvalidateVisual();
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
_constraint = availableSize;
|
|
|
|
_textLayout?.Dispose();
|
|
_textLayout = null;
|
|
|
|
InvalidateArrange();
|
|
|
|
var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
|
|
|
|
return new Size(textWidth, TextLayout.Height);
|
|
}
|
|
|
|
protected override Size ArrangeOverride(Size finalSize)
|
|
{
|
|
var finalWidth = finalSize.Width;
|
|
|
|
var textWidth = Math.Ceiling(TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing);
|
|
|
|
if (finalSize.Width < textWidth)
|
|
{
|
|
finalSize = finalSize.WithWidth(textWidth);
|
|
}
|
|
|
|
// Check if the '_constraint' has changed since the last measure,
|
|
// if so recalculate the TextLayout according to the new size
|
|
// NOTE: It is important to check this against the actual final size
|
|
// (excluding the trailing whitespace) to avoid TextLayout overflow.
|
|
if (MathUtilities.AreClose(_constraint.Width, finalWidth) == false)
|
|
{
|
|
_constraint = new Size(Math.Ceiling(finalWidth), double.PositiveInfinity);
|
|
|
|
_textLayout?.Dispose();
|
|
_textLayout = null;
|
|
}
|
|
|
|
return finalSize;
|
|
}
|
|
|
|
private void CaretTimerTick(object? sender, EventArgs e)
|
|
{
|
|
_caretBlink = !_caretBlink;
|
|
|
|
InvalidateVisual();
|
|
}
|
|
|
|
public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
|
|
{
|
|
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge);
|
|
var textLine = TextLayout.TextLines[lineIndex];
|
|
|
|
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
|
|
|
|
var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
|
|
|
|
if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition)
|
|
{
|
|
characterHit = nextCaretCharacterHit;
|
|
}
|
|
|
|
if (textPosition == characterHit.FirstCharacterIndex + characterHit.TrailingLength)
|
|
{
|
|
UpdateCaret(characterHit);
|
|
}
|
|
else
|
|
{
|
|
UpdateCaret(trailingEdge ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex));
|
|
}
|
|
|
|
_navigationPosition = _caretBounds.Position;
|
|
|
|
CaretChanged();
|
|
}
|
|
|
|
public void MoveCaretToPoint(Point point)
|
|
{
|
|
var hit = TextLayout.HitTestPoint(point);
|
|
|
|
UpdateCaret(hit.CharacterHit);
|
|
|
|
_navigationPosition = _caretBounds.Position;
|
|
|
|
CaretChanged();
|
|
}
|
|
|
|
public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
|
|
{
|
|
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0);
|
|
|
|
if (lineIndex < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var (currentX, currentY) = _navigationPosition;
|
|
|
|
if (direction == LogicalDirection.Forward)
|
|
{
|
|
if (lineIndex + 1 > TextLayout.TextLines.Count - 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var textLine = TextLayout.TextLines[lineIndex];
|
|
|
|
currentY += textLine.Height;
|
|
}
|
|
else
|
|
{
|
|
if (lineIndex - 1 < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var textLine = TextLayout.TextLines[--lineIndex];
|
|
|
|
currentY -= textLine.Height;
|
|
}
|
|
|
|
var navigationPosition = _navigationPosition;
|
|
|
|
MoveCaretToPoint(new Point(currentX, currentY));
|
|
|
|
_navigationPosition = navigationPosition.WithY(_caretBounds.Y);
|
|
|
|
CaretChanged();
|
|
}
|
|
|
|
private void EnsureCaretTimer()
|
|
{
|
|
if (_caretTimer == null)
|
|
{
|
|
ResetCaretTimer();
|
|
}
|
|
}
|
|
|
|
private void ResetCaretTimer()
|
|
{
|
|
bool isEnabled = false;
|
|
|
|
if (_caretTimer != null)
|
|
{
|
|
_caretTimer.Tick -= CaretTimerTick;
|
|
|
|
if (_caretTimer.IsEnabled)
|
|
{
|
|
_caretTimer.Stop();
|
|
isEnabled = true;
|
|
}
|
|
|
|
_caretTimer = null;
|
|
}
|
|
|
|
if (CaretBlinkInterval.TotalMilliseconds > 0)
|
|
{
|
|
_caretTimer = new DispatcherTimer { Interval = CaretBlinkInterval };
|
|
_caretTimer.Tick += CaretTimerTick;
|
|
|
|
if (isEnabled)
|
|
_caretTimer.Start();
|
|
}
|
|
}
|
|
|
|
public CharacterHit GetNextCharacterHit(LogicalDirection direction = LogicalDirection.Forward)
|
|
{
|
|
if (Text is null)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
var characterHit = _lastCharacterHit;
|
|
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
|
|
|
|
if (lineIndex < 0)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
if (direction == LogicalDirection.Forward)
|
|
{
|
|
while (lineIndex < TextLayout.TextLines.Count)
|
|
{
|
|
var textLine = TextLayout.TextLines[lineIndex];
|
|
|
|
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
|
|
|
|
caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
if (textLine.TrailingWhitespaceLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length)
|
|
{
|
|
characterHit = new CharacterHit(caretIndex);
|
|
}
|
|
|
|
if (caretIndex >= Text.Length)
|
|
{
|
|
characterHit = new CharacterHit(Text.Length);
|
|
|
|
break;
|
|
}
|
|
|
|
if (caretIndex - textLine.NewLineLength == textLine.FirstTextSourceIndex + textLine.Length)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (caretIndex <= CaretIndex)
|
|
{
|
|
lineIndex++;
|
|
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (lineIndex >= 0)
|
|
{
|
|
var textLine = TextLayout.TextLines[lineIndex];
|
|
|
|
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
|
|
|
|
caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
if (caretIndex >= CaretIndex)
|
|
{
|
|
lineIndex--;
|
|
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return characterHit;
|
|
}
|
|
|
|
public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
|
|
{
|
|
if (FlowDirection == FlowDirection.RightToLeft)
|
|
{
|
|
direction = direction == LogicalDirection.Forward ?
|
|
LogicalDirection.Backward :
|
|
LogicalDirection.Forward;
|
|
}
|
|
|
|
var characterHit = GetNextCharacterHit(direction);
|
|
|
|
UpdateCaret(characterHit);
|
|
|
|
_navigationPosition = _caretBounds.Position;
|
|
|
|
CaretChanged();
|
|
}
|
|
|
|
internal void UpdateCaret(CharacterHit characterHit, bool notify = true)
|
|
{
|
|
_lastCharacterHit = characterHit;
|
|
|
|
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0);
|
|
var textLine = TextLayout.TextLines[lineIndex];
|
|
var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
|
|
|
|
var distanceY = 0d;
|
|
|
|
for (var i = 0; i < lineIndex; i++)
|
|
{
|
|
var currentLine = TextLayout.TextLines[i];
|
|
|
|
distanceY += currentLine.Height;
|
|
}
|
|
|
|
var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height);
|
|
|
|
if (caretBounds != _caretBounds)
|
|
{
|
|
_caretBounds = caretBounds;
|
|
|
|
CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
if (notify)
|
|
{
|
|
SetCurrentValue(CaretIndexProperty, caretIndex);
|
|
}
|
|
}
|
|
|
|
internal Rect GetCursorRectangle()
|
|
{
|
|
return _caretBounds;
|
|
}
|
|
|
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
base.OnAttachedToVisualTree(e);
|
|
|
|
ResetCaretTimer();
|
|
}
|
|
|
|
private void EnsureTextSelectionLayer()
|
|
{
|
|
if (TextSelectionHandleCanvas == null)
|
|
{
|
|
TextSelectionHandleCanvas = new TextSelectionHandleCanvas();
|
|
}
|
|
TextSelectionHandleCanvas.SetPresenter(this);
|
|
_layer = TextSelectorLayer.GetTextSelectorLayer(this);
|
|
if (TextSelectionHandleCanvas.VisualParent is { } parent && parent != _layer)
|
|
{
|
|
if (parent is TextSelectorLayer l)
|
|
{
|
|
l.Remove(TextSelectionHandleCanvas);
|
|
}
|
|
}
|
|
if (_layer != null && TextSelectionHandleCanvas.VisualParent != _layer)
|
|
_layer?.Add(TextSelectionHandleCanvas);
|
|
}
|
|
|
|
private void RemoveTextSelectionCanvas()
|
|
{
|
|
if(_layer != null && TextSelectionHandleCanvas is { } canvas)
|
|
{
|
|
canvas.SetPresenter(null);
|
|
_layer.Remove(canvas);
|
|
}
|
|
|
|
TextSelectionHandleCanvas = null;
|
|
}
|
|
|
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
|
{
|
|
base.OnDetachedFromVisualTree(e);
|
|
if (TextSelectionHandleCanvas is { } c)
|
|
{
|
|
_layer?.Remove(c);
|
|
c.SetPresenter(null);
|
|
}
|
|
|
|
if (_caretTimer != null)
|
|
{
|
|
_caretTimer.Stop();
|
|
_caretTimer.Tick -= CaretTimerTick;
|
|
}
|
|
}
|
|
|
|
private void OnPreeditChanged(string? preeditText, int? cursorPosition)
|
|
{
|
|
if (string.IsNullOrEmpty(preeditText))
|
|
{
|
|
UpdateCaret(new CharacterHit(CaretIndex), false);
|
|
}
|
|
else
|
|
{
|
|
var cursorPos = cursorPosition is >= 0 && cursorPosition <= preeditText.Length
|
|
? cursorPosition.Value
|
|
: preeditText.Length;
|
|
UpdateCaret(new CharacterHit(CaretIndex + cursorPos), false);
|
|
InvalidateMeasure();
|
|
CaretChanged();
|
|
}
|
|
}
|
|
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
|
|
if (change.Property == CaretIndexProperty)
|
|
{
|
|
MoveCaretToTextPosition(change.GetNewValue<int>());
|
|
}
|
|
|
|
if(change.Property == PreeditTextProperty)
|
|
{
|
|
OnPreeditChanged(change.NewValue as string, PreeditTextCursorPosition);
|
|
}
|
|
|
|
if(change.Property == PreeditTextCursorPositionProperty)
|
|
{
|
|
OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
|
|
}
|
|
|
|
if(change.Property == TextProperty)
|
|
{
|
|
if (!string.IsNullOrEmpty(PreeditText))
|
|
{
|
|
SetCurrentValue(PreeditTextProperty, null);
|
|
}
|
|
}
|
|
|
|
if(change.Property == CaretIndexProperty)
|
|
{
|
|
if (!string.IsNullOrEmpty(PreeditText))
|
|
{
|
|
SetCurrentValue(PreeditTextProperty, null);
|
|
}
|
|
}
|
|
|
|
if (change.Property == CaretBlinkIntervalProperty)
|
|
{
|
|
ResetCaretTimer();
|
|
}
|
|
|
|
switch (change.Property.Name)
|
|
{
|
|
case nameof(PreeditText):
|
|
case nameof(Foreground):
|
|
case nameof(FontSize):
|
|
case nameof(FontStyle):
|
|
case nameof(FontWeight):
|
|
case nameof(FontFamily):
|
|
case nameof(FontStretch):
|
|
|
|
case nameof(Text):
|
|
case nameof(TextAlignment):
|
|
case nameof(TextWrapping):
|
|
|
|
case nameof(LineHeight):
|
|
case nameof(LetterSpacing):
|
|
|
|
case nameof(SelectionStart):
|
|
case nameof(SelectionEnd):
|
|
case nameof(SelectionForegroundBrush):
|
|
case nameof(ShowSelectionHighlightProperty):
|
|
|
|
case nameof(PasswordChar):
|
|
case nameof(RevealPassword):
|
|
case nameof(FlowDirection):
|
|
{
|
|
InvalidateTextLayout();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|