// Copyright (c) The Perspex 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 Perspex.Media; using Perspex.Styling; using Perspex.Threading; using Perspex.VisualTree; namespace Perspex.Controls.Presenters { public class TextPresenter : TextBlock { public static readonly StyledProperty CaretIndexProperty = TextBox.CaretIndexProperty.AddOwner(); public static readonly StyledProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner(); public static readonly StyledProperty SelectionEndProperty = TextBox.SelectionEndProperty.AddOwner(); private readonly DispatcherTimer _caretTimer; private bool _caretBlink; private IBrush _highlightBrush; static TextPresenter() { CaretIndexProperty.OverrideValidation((o, v) => v); } public TextPresenter() { _caretTimer = new DispatcherTimer(); _caretTimer.Interval = TimeSpan.FromMilliseconds(500); _caretTimer.Tick += CaretTimerTick; Observable.Merge( this.GetObservable(SelectionStartProperty), this.GetObservable(SelectionEndProperty)) .Subscribe(_ => InvalidateFormattedText()); this.GetObservable(CaretIndexProperty) .Subscribe(CaretIndexChanged); } public int CaretIndex { get { return GetValue(CaretIndexProperty); } set { SetValue(CaretIndexProperty, value); } } public int SelectionStart { get { return GetValue(SelectionStartProperty); } set { SetValue(SelectionStartProperty, value); } } public int SelectionEnd { get { return GetValue(SelectionEndProperty); } set { SetValue(SelectionEndProperty, value); } } public int GetCaretIndex(Point point) { var hit = FormattedText.HitTestPoint(point); return hit.TextPosition + (hit.IsTrailing ? 1 : 0); } 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; var rects = FormattedText.HitTestTextRange(start, length); if (_highlightBrush == null) { _highlightBrush = (IBrush)this.FindStyleResource("HighlightBrush"); } foreach (var rect in rects) { context.FillRectangle(_highlightBrush, rect); } } base.Render(context); if (selectionStart == selectionEnd) { var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; var caretBrush = Brushes.Black; 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)); } 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) { _caretBlink = true; _caretTimer.Stop(); _caretTimer.Start(); InvalidateVisual(); 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.InvokeAsync( () => { var rect = FormattedText.HitTestTextPosition(caretIndex); this.BringIntoView(rect); }, DispatcherPriority.Normal); } } } protected override FormattedText CreateFormattedText(Size constraint) { var result = base.CreateFormattedText(constraint); var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; if (length > 0) { result.SetForegroundBrush(Brushes.White, start, length); } return result; } protected override Size MeasureOverride(Size availableSize) { var text = Text; if (!string.IsNullOrWhiteSpace(text)) { return base.MeasureOverride(availableSize); } else { // TODO: Pretty sure that measuring "X" isn't the right way to do this... using (var formattedText = new FormattedText( "X", FontFamily, FontSize, FontStyle, TextAlignment, FontWeight)) { return formattedText.Measure(); } } } private void CaretTimerTick(object sender, EventArgs e) { _caretBlink = !_caretBlink; InvalidateVisual(); } } }