A cross-platform UI framework for .NET
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.
 
 
 

215 lines
7.0 KiB

// 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<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>();
public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>();
public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>();
private readonly DispatcherTimer _caretTimer;
private bool _caretBlink;
private IBrush _highlightBrush;
static TextPresenter()
{
CaretIndexProperty.OverrideValidation<TextPresenter>((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();
}
}
}