// 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 CaretIndexProperty = TextBox.CaretIndexProperty.AddOwner( o => o.CaretIndex, (o, v) => o.CaretIndex = v); public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrushProperty)); public static readonly StyledProperty SelectionForegroundBrushProperty = AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); public static readonly StyledProperty CaretBrushProperty = AvaloniaProperty.Register(nameof(CaretBrushProperty)); public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( o => o.SelectionStart, (o, v) => o.SelectionStart = v); public static readonly DirectProperty SelectionEndProperty = TextBox.SelectionEndProperty.AddOwner( o => o.SelectionEnd, (o, v) => o.SelectionEnd = v); /// /// Defines the property. /// public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, (o, v) => o.Text = v); /// /// Defines the property. /// public static readonly StyledProperty TextAlignmentProperty = TextBlock.TextAlignmentProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); 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(PasswordCharProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, SelectionStartProperty, SelectionEndProperty); Observable.Merge( SelectionStartProperty.Changed, SelectionEndProperty.Changed, PasswordCharProperty.Changed ).AddClassHandler((x,_) => x.InvalidateFormattedText()); CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); } public TextPresenter() { _text = string.Empty; _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _caretTimer.Tick += CaretTimerTick; } /// /// Gets or sets a brush used to paint the control's background. /// public IBrush Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } /// /// Gets or sets the text. /// [Content] public string Text { get => _text; set => SetAndRaise(TextProperty, ref _text, value); } /// /// Gets or sets the font family. /// public FontFamily FontFamily { get => TextBlock.GetFontFamily(this); set => TextBlock.SetFontFamily(this, value); } /// /// Gets or sets the font size. /// public double FontSize { get => TextBlock.GetFontSize(this); set => TextBlock.SetFontSize(this, value); } /// /// Gets or sets the font style. /// public FontStyle FontStyle { get => TextBlock.GetFontStyle(this); set => TextBlock.SetFontStyle(this, value); } /// /// Gets or sets the font weight. /// public FontWeight FontWeight { get => TextBlock.GetFontWeight(this); set => TextBlock.SetFontWeight(this, value); } /// /// Gets or sets a brush used to paint the text. /// public IBrush Foreground { get => TextBlock.GetForeground(this); set => TextBlock.SetForeground(this, value); } /// /// Gets or sets the control's text wrapping mode. /// public TextWrapping TextWrapping { get => GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } /// /// Gets or sets the text alignment. /// public TextAlignment TextAlignment { get => GetValue(TextAlignmentProperty); set => SetValue(TextAlignmentProperty, value); } /// /// Gets the used to render the text. /// 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); } /// /// Creates the used to render the text. /// /// The constraint of the text. /// The text to format. /// A object. 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, }; } /// /// Invalidates . /// protected void InvalidateFormattedText() { if (_formattedText != null) { _constraint = _formattedText.Constraint; _formattedText = null; } } /// /// Renders the to a drawing context. /// /// The drawing context. 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); } } } /// /// Creates the used to render the text. /// /// The constraint of the text. /// The text to generated the for. /// A object. 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; } /// /// Measures the control. /// /// The available size for the control. /// The desired size. 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(); } } }