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.
215 lines
7.0 KiB
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();
|
|
}
|
|
}
|
|
}
|
|
|