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.
520 lines
17 KiB
520 lines
17 KiB
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
using System;
|
|
using System.Reactive.Linq;
|
|
using Avalonia.Media;
|
|
using Avalonia.Metadata;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
|
|
namespace Avalonia.Controls.Presenters
|
|
{
|
|
public class TextPresenter : Control
|
|
{
|
|
public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty =
|
|
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
|
|
o => o.CaretIndex,
|
|
(o, v) => o.CaretIndex = v);
|
|
|
|
public static readonly StyledProperty<char> PasswordCharProperty =
|
|
AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar));
|
|
|
|
public static readonly StyledProperty<IBrush> SelectionBrushProperty =
|
|
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionBrushProperty));
|
|
|
|
public static readonly StyledProperty<IBrush> SelectionForegroundBrushProperty =
|
|
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionForegroundBrushProperty));
|
|
|
|
public static readonly StyledProperty<IBrush> CaretBrushProperty =
|
|
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(CaretBrushProperty));
|
|
|
|
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
|
|
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
|
|
o => o.SelectionStart,
|
|
(o, v) => o.SelectionStart = v);
|
|
|
|
public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty =
|
|
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(
|
|
o => o.SelectionEnd,
|
|
(o, v) => o.SelectionEnd = v);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="Text"/> property.
|
|
/// </summary>
|
|
public static readonly DirectProperty<TextPresenter, string> TextProperty =
|
|
AvaloniaProperty.RegisterDirect<TextPresenter, string>(
|
|
nameof(Text),
|
|
o => o.Text,
|
|
(o, v) => o.Text = v);
|
|
|
|
/// <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="Background"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<IBrush> BackgroundProperty =
|
|
Border.BackgroundProperty.AddOwner<TextPresenter>();
|
|
|
|
private readonly DispatcherTimer _caretTimer;
|
|
private int _caretIndex;
|
|
private int _selectionStart;
|
|
private int _selectionEnd;
|
|
private bool _caretBlink;
|
|
private string _text;
|
|
private FormattedText _formattedText;
|
|
private Size _constraint;
|
|
|
|
static TextPresenter()
|
|
{
|
|
AffectsRender<TextPresenter>(PasswordCharProperty,
|
|
SelectionBrushProperty, SelectionForegroundBrushProperty,
|
|
SelectionStartProperty, SelectionEndProperty);
|
|
|
|
Observable.Merge(
|
|
TextProperty.Changed,
|
|
SelectionStartProperty.Changed,
|
|
SelectionEndProperty.Changed,
|
|
PasswordCharProperty.Changed
|
|
).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
|
|
|
|
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
|
|
}
|
|
|
|
public TextPresenter()
|
|
{
|
|
_text = string.Empty;
|
|
_caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
|
_caretTimer.Tick += CaretTimerTick;
|
|
}
|
|
|
|
/// <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 the text.
|
|
/// </summary>
|
|
[Content]
|
|
public string Text
|
|
{
|
|
get => _text;
|
|
set => SetAndRaise(TextProperty, ref _text, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font family.
|
|
/// </summary>
|
|
public FontFamily FontFamily
|
|
{
|
|
get => TextBlock.GetFontFamily(this);
|
|
set => TextBlock.SetFontFamily(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font size.
|
|
/// </summary>
|
|
public double FontSize
|
|
{
|
|
get => TextBlock.GetFontSize(this);
|
|
set => TextBlock.SetFontSize(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font style.
|
|
/// </summary>
|
|
public FontStyle FontStyle
|
|
{
|
|
get => TextBlock.GetFontStyle(this);
|
|
set => TextBlock.SetFontStyle(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font weight.
|
|
/// </summary>
|
|
public FontWeight FontWeight
|
|
{
|
|
get => TextBlock.GetFontWeight(this);
|
|
set => TextBlock.SetFontWeight(this, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a brush used to paint the text.
|
|
/// </summary>
|
|
public IBrush Foreground
|
|
{
|
|
get => TextBlock.GetForeground(this);
|
|
set => TextBlock.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 text alignment.
|
|
/// </summary>
|
|
public TextAlignment TextAlignment
|
|
{
|
|
get => GetValue(TextAlignmentProperty);
|
|
set => SetValue(TextAlignmentProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="FormattedText"/> used to render the text.
|
|
/// </summary>
|
|
public FormattedText FormattedText
|
|
{
|
|
get
|
|
{
|
|
return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text));
|
|
}
|
|
}
|
|
|
|
public int CaretIndex
|
|
{
|
|
get
|
|
{
|
|
return _caretIndex;
|
|
}
|
|
|
|
set
|
|
{
|
|
value = CoerceCaretIndex(value);
|
|
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
|
|
}
|
|
}
|
|
|
|
public char PasswordChar
|
|
{
|
|
get => GetValue(PasswordCharProperty);
|
|
set => SetValue(PasswordCharProperty, 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);
|
|
}
|
|
|
|
public int SelectionStart
|
|
{
|
|
get
|
|
{
|
|
return _selectionStart;
|
|
}
|
|
|
|
set
|
|
{
|
|
value = CoerceCaretIndex(value);
|
|
SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
|
|
}
|
|
}
|
|
|
|
public int SelectionEnd
|
|
{
|
|
get
|
|
{
|
|
return _selectionEnd;
|
|
}
|
|
|
|
set
|
|
{
|
|
value = CoerceCaretIndex(value);
|
|
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
|
|
}
|
|
}
|
|
|
|
public int GetCaretIndex(Point point)
|
|
{
|
|
var hit = FormattedText.HitTestPoint(point);
|
|
return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the <see cref="FormattedText"/> used to render the text.
|
|
/// </summary>
|
|
/// <param name="constraint">The constraint of the text.</param>
|
|
/// <param name="text">The text to format.</param>
|
|
/// <returns>A <see cref="FormattedText"/> object.</returns>
|
|
private FormattedText CreateFormattedTextInternal(Size constraint, string text)
|
|
{
|
|
return new FormattedText
|
|
{
|
|
Constraint = constraint,
|
|
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
|
|
FontSize = FontSize,
|
|
Text = text ?? string.Empty,
|
|
TextAlignment = TextAlignment,
|
|
TextWrapping = TextWrapping,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invalidates <see cref="FormattedText"/>.
|
|
/// </summary>
|
|
protected void InvalidateFormattedText()
|
|
{
|
|
if (_formattedText != null)
|
|
{
|
|
_constraint = _formattedText.Constraint;
|
|
_formattedText = null;
|
|
}
|
|
|
|
InvalidateVisual();
|
|
}
|
|
|
|
/// <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));
|
|
}
|
|
|
|
FormattedText.Constraint = Bounds.Size;
|
|
context.DrawText(Foreground, new Point(), FormattedText);
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
var selectionStart = SelectionStart;
|
|
var selectionEnd = SelectionEnd;
|
|
|
|
if (selectionStart != selectionEnd)
|
|
{
|
|
var start = Math.Min(selectionStart, selectionEnd);
|
|
var length = Math.Max(selectionStart, selectionEnd) - start;
|
|
|
|
// issue #600: set constraint before any FormattedText manipulation
|
|
// see base.Render(...) implementation
|
|
FormattedText.Constraint = _constraint;
|
|
|
|
var rects = FormattedText.HitTestTextRange(start, length);
|
|
|
|
foreach (var rect in rects)
|
|
{
|
|
context.FillRectangle(SelectionBrush, rect);
|
|
}
|
|
}
|
|
|
|
RenderInternal(context);
|
|
|
|
if (selectionStart == selectionEnd)
|
|
{
|
|
var caretBrush = CaretBrush;
|
|
|
|
if (caretBrush is null)
|
|
{
|
|
var backgroundColor = (Background as SolidColorBrush)?.Color;
|
|
if (backgroundColor.HasValue)
|
|
{
|
|
byte red = (byte)~(backgroundColor.Value.R);
|
|
byte green = (byte)~(backgroundColor.Value.G);
|
|
byte blue = (byte)~(backgroundColor.Value.B);
|
|
|
|
caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue));
|
|
}
|
|
else
|
|
caretBrush = Brushes.Black;
|
|
}
|
|
|
|
if (_caretBlink)
|
|
{
|
|
var charPos = FormattedText.HitTestTextPosition(CaretIndex);
|
|
var x = Math.Floor(charPos.X) + 0.5;
|
|
var y = Math.Floor(charPos.Y) + 0.5;
|
|
var b = Math.Ceiling(charPos.Bottom) - 0.5;
|
|
|
|
context.DrawLine(
|
|
new Pen(caretBrush, 1),
|
|
new Point(x, y),
|
|
new Point(x, b));
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ShowCaret()
|
|
{
|
|
_caretBlink = true;
|
|
_caretTimer.Start();
|
|
InvalidateVisual();
|
|
}
|
|
|
|
public void HideCaret()
|
|
{
|
|
_caretBlink = false;
|
|
_caretTimer.Stop();
|
|
InvalidateVisual();
|
|
}
|
|
|
|
internal void CaretIndexChanged(int caretIndex)
|
|
{
|
|
if (this.GetVisualParent() != null)
|
|
{
|
|
if (_caretTimer.IsEnabled)
|
|
{
|
|
_caretBlink = true;
|
|
_caretTimer.Stop();
|
|
_caretTimer.Start();
|
|
InvalidateVisual();
|
|
}
|
|
else
|
|
{
|
|
_caretTimer.Start();
|
|
InvalidateVisual();
|
|
_caretTimer.Stop();
|
|
}
|
|
|
|
if (IsMeasureValid)
|
|
{
|
|
var rect = FormattedText.HitTestTextPosition(caretIndex);
|
|
this.BringIntoView(rect);
|
|
}
|
|
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(
|
|
() =>
|
|
{
|
|
var rect = FormattedText.HitTestTextPosition(caretIndex);
|
|
this.BringIntoView(rect);
|
|
},
|
|
DispatcherPriority.Render);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the <see cref="FormattedText"/> used to render the text.
|
|
/// </summary>
|
|
/// <param name="constraint">The constraint of the text.</param>
|
|
/// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param>
|
|
/// <returns>A <see cref="FormattedText"/> object.</returns>
|
|
protected virtual FormattedText CreateFormattedText(Size constraint, string text)
|
|
{
|
|
FormattedText result = null;
|
|
|
|
if (PasswordChar != default(char))
|
|
{
|
|
result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0));
|
|
}
|
|
else
|
|
{
|
|
result = CreateFormattedTextInternal(constraint, text);
|
|
}
|
|
|
|
var selectionStart = SelectionStart;
|
|
var selectionEnd = SelectionEnd;
|
|
var start = Math.Min(selectionStart, selectionEnd);
|
|
var length = Math.Max(selectionStart, selectionEnd) - start;
|
|
|
|
if (length > 0)
|
|
{
|
|
result.Spans = new[]
|
|
{
|
|
new FormattedTextStyleSpan(start, length, SelectionForegroundBrush),
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Measures the control.
|
|
/// </summary>
|
|
/// <param name="availableSize">The available size for the control.</param>
|
|
/// <returns>The desired size.</returns>
|
|
private Size MeasureInternal(Size availableSize)
|
|
{
|
|
if (!string.IsNullOrEmpty(Text))
|
|
{
|
|
if (TextWrapping == TextWrapping.Wrap)
|
|
{
|
|
FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
|
|
}
|
|
else
|
|
{
|
|
FormattedText.Constraint = Size.Infinity;
|
|
}
|
|
|
|
return FormattedText.Bounds.Size;
|
|
}
|
|
|
|
return new Size();
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
var text = Text;
|
|
|
|
if (!string.IsNullOrEmpty(text))
|
|
{
|
|
return MeasureInternal(availableSize);
|
|
}
|
|
else
|
|
{
|
|
return new FormattedText
|
|
{
|
|
Text = "X",
|
|
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
|
|
FontSize = FontSize,
|
|
TextAlignment = TextAlignment,
|
|
Constraint = availableSize,
|
|
}.Bounds.Size;
|
|
}
|
|
}
|
|
|
|
private int CoerceCaretIndex(int value)
|
|
{
|
|
var text = Text;
|
|
var length = text?.Length ?? 0;
|
|
return Math.Max(0, Math.Min(length, value));
|
|
}
|
|
|
|
private void CaretTimerTick(object sender, EventArgs e)
|
|
{
|
|
_caretBlink = !_caretBlink;
|
|
InvalidateVisual();
|
|
}
|
|
}
|
|
}
|
|
|