diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs
index 054d93ddbb9e..05c8745436e0 100644
--- a/src/Uno.UI/FeatureConfiguration.cs
+++ b/src/Uno.UI/FeatureConfiguration.cs
@@ -503,6 +503,12 @@ public static class TextBox
/// This feature is used to avoid screenshot comparisons false positives
public static bool HideCaret { get; set; }
+ ///
+ /// Determines if a native (Gtk/Wpf) TextBox overlay should be used on the skia targets instead of the
+ /// Uno skia-based TextBox implementation.
+ ///
+ public static bool UseOverlayOnSkia { get; set; } = false;
+
#if __ANDROID__
///
/// The legacy prevents invalid input on hardware keyboard.
diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs
index 4a556c8ab7ed..f6a445a918b4 100644
--- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs
@@ -104,7 +104,7 @@ internal override void OnPropertyChanged2(DependencyPropertyChangedEventArgs arg
private Hyperlink? FindHyperlinkAt(Point point)
{
var padding = Padding;
- var span = Inlines.GetRenderSegmentSpanAt(point - new Point(padding.Left, padding.Top), false);
+ var span = Inlines.GetRenderSegmentSpanAt(point - new Point(padding.Left, padding.Top), false)?.span;
if (span == null)
{
diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs
index 8b0c7bc464bd..414a95043bee 100644
--- a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.cs
@@ -22,6 +22,7 @@
using Uno.UI.Helpers;
using Uno.UI.Xaml.Media;
using Windows.ApplicationModel.DataTransfer;
+using Uno.UI;
#if HAS_UNO_WINUI
using Microsoft.UI.Input;
@@ -116,6 +117,10 @@ public TextBox()
DefaultStyleKey = typeof(TextBox);
SizeChanged += OnSizeChanged;
+
+#if __SKIA__
+ _timer.Tick += TimerOnTick;
+#endif
}
private protected override void OnLoaded()
@@ -150,10 +155,15 @@ private protected override void OnLoaded()
// When support for TemplateBinding for attached DPs was added, TextBox broke (test: TextBox_AutoGrow_Vertically_Wrapping_Test) because of
// change in the values of these properties. The following code serves as a workaround to set the values to what they used to be
// before the support for TemplateBinding for attached DPs.
- scrollViewer.HorizontalScrollMode = ScrollMode.Enabled; // The template sets this to Auto
- scrollViewer.VerticalScrollMode = ScrollMode.Enabled; // The template sets this to Auto
- scrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; // The template sets this to Hidden
- scrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; // The template sets this to Hidden
+#if __SKIA__
+ if (FeatureConfiguration.TextBox.UseOverlayOnSkia)
+#endif
+ {
+ scrollViewer.HorizontalScrollMode = ScrollMode.Enabled; // The template sets this to Auto
+ scrollViewer.VerticalScrollMode = ScrollMode.Enabled; // The template sets this to Auto
+ scrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; // The template sets this to Hidden
+ scrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; // The template sets this to Hidden
+ }
#endif
}
}
@@ -896,6 +906,14 @@ private void OnFocusStateChanged(FocusState oldValue, FocusState newValue, bool
}
UpdateVisualState();
+#if __SKIA__
+ if (!FeatureConfiguration.TextBox.UseOverlayOnSkia)
+ {
+ // this is needed so that we UpdateScrolling after the button appears.
+ UpdateLayout();
+ UpdateScrolling();
+ }
+#endif
}
partial void OnFocusStateChangedPartial(FocusState focusState);
@@ -946,8 +964,12 @@ protected override void OnPointerPressed(PointerRoutedEventArgs args)
}
args.Handled = true;
+
+ OnPointerPressedNative(args);
}
+ partial void OnPointerPressedNative(PointerRoutedEventArgs e);
+
///
protected override void OnPointerReleased(PointerRoutedEventArgs args)
{
@@ -971,7 +993,16 @@ protected override void OnTapped(TappedRoutedEventArgs e)
partial void OnTappedPartial();
///
- protected override void OnKeyDown(KeyRoutedEventArgs args)
+ protected override void OnKeyDown(KeyRoutedEventArgs args) => OnKeyDownPartial(args);
+
+ private partial void OnKeyDownPartial(KeyRoutedEventArgs args);
+
+#if !__SKIA__
+ private partial void OnKeyDownPartial(KeyRoutedEventArgs args) => OnKeyDownInternal(args);
+
+#endif
+
+ private void OnKeyDownInternal(KeyRoutedEventArgs args)
{
base.OnKeyDown(args);
diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs
index 58d74151959e..1958f0cc3145 100644
--- a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs
@@ -1,10 +1,24 @@
-using Windows.UI.Xaml.Media;
+using System;
+using System.Collections.Generic;
+using Windows.Foundation;
+using Windows.System;
+using Windows.UI.Xaml.Documents;
+using Windows.UI.Xaml.Input;
+using Windows.UI.Xaml.Media;
+using Uno.UI;
namespace Windows.UI.Xaml.Controls;
public partial class TextBox
{
private TextBoxView _textBoxView;
+ private (int start, int length) _selection;
+ private bool _selectionEndsAtTheStart;
+ private bool _showCaret = true;
+ private readonly DispatcherTimer _timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromSeconds(0.5)
+ };
internal TextBoxView TextBoxView => _textBoxView;
@@ -38,24 +52,304 @@ private void UpdateTextBoxView()
}
}
- partial void OnFocusStateChangedPartial(FocusState focusState) => TextBoxView?.OnFocusStateChanged(focusState);
+ partial void OnFocusStateChangedPartial(FocusState focusState)
+ {
+ if (FeatureConfiguration.TextBox.UseOverlayOnSkia)
+ {
+ TextBoxView?.OnFocusStateChanged(focusState);
+ }
+ else
+ {
+ if (focusState != FocusState.Unfocused)
+ {
+ _showCaret = true;
+ _timer.Start();
+ }
+ else
+ {
+ _showCaret = false;
+ _timer.Stop();
+ }
+ UpdateDisplaySelection();
+ }
+ }
partial void SelectPartial(int start, int length)
{
- TextBoxView?.Select(start, length);
+ _selectionEndsAtTheStart = false;
+ _selection = (start, length);
+ if (FeatureConfiguration.TextBox.UseOverlayOnSkia)
+ {
+ TextBoxView?.Select(start, length);
+ }
+ else
+ {
+ _timer.Stop();
+ _showCaret = true;
+ _timer.Start();
+ UpdateDisplaySelection();
+ UpdateLayout();
+ UpdateScrolling();
+ }
}
partial void SelectAllPartial() => Select(0, Text.Length);
public int SelectionStart
{
- get => TextBoxView?.GetSelectionStart() ?? 0;
+ get => FeatureConfiguration.TextBox.UseOverlayOnSkia ? TextBoxView?.GetSelectionStart() ?? 0 : _selection.start;
set => Select(start: value, length: SelectionLength);
}
public int SelectionLength
{
- get => TextBoxView?.GetSelectionLength() ?? 0;
+ get => FeatureConfiguration.TextBox.UseOverlayOnSkia ? TextBoxView?.GetSelectionLength() ?? 0 : _selection.length;
set => Select(SelectionStart, value);
}
+
+ internal void UpdateDisplaySelection()
+ {
+ if (TextBoxView?.DisplayBlock.Inlines is { } inlines)
+ {
+ inlines.Selection = (0, SelectionStart, 0, SelectionStart + SelectionLength);
+ inlines.RenderSelectionAndCaret = FocusState != FocusState.Unfocused;
+ var showCaret = _showCaret && !FeatureConfiguration.TextBox.HideCaret && !IsReadOnly && _selection.length == 0;
+ inlines.Caret = (!_selectionEndsAtTheStart, showCaret ? Colors.Black : Colors.Transparent);
+ TextBoxView?.DisplayBlock.InvalidateInlines(true);
+ }
+ }
+
+ private void UpdateScrolling()
+ {
+ if (!FeatureConfiguration.TextBox.UseOverlayOnSkia && _contentElement is ScrollViewer sv)
+ {
+ var selectionEnd = _selectionEndsAtTheStart ? _selection.start : _selection.start + _selection.length;
+
+ var horizontalOffset = sv.HorizontalOffset;
+ var verticalOffset = sv.VerticalOffset;
+
+ var rect = TextBoxView.DisplayBlock.Inlines.GetRectForTextBlockIndex(selectionEnd);
+
+ var newHorizontalOffset = horizontalOffset.AtMost(rect.Left).AtLeast(rect.Left - sv.ViewportWidth + rect.Height * InlineCollection.CaretThicknessAsRatioOfLineHeight);
+ var newVerticalOffset = verticalOffset.AtMost(rect.Top).AtLeast(rect.Top - sv.ViewportWidth);
+
+ sv.ChangeView(newHorizontalOffset, newVerticalOffset, null);
+ }
+ }
+
+ private partial void OnKeyDownPartial(KeyRoutedEventArgs args)
+ {
+ if (FeatureConfiguration.TextBox.UseOverlayOnSkia)
+ {
+ OnKeyDownInternal(args);
+ return;
+ }
+
+ base.OnKeyDown(args);
+
+ // Note: On windows only keys that are "moving the cursor" are handled
+ // AND ** only KeyDown ** is handled (not KeyUp)
+
+ // move to possibly-negative selection length format
+ var (selectionStart, selectionLength) = _selectionEndsAtTheStart ? (_selection.start + _selection.length, -_selection.length) : (_selection.start, _selection.length);
+
+ var text = Text;
+ var shift = args.KeyboardModifiers.HasFlag(VirtualKeyModifiers.Shift);
+ switch (args.Key)
+ {
+ case VirtualKey.Up:
+ if (shift)
+ {
+ selectionLength = -selectionStart;
+ }
+ else
+ {
+ selectionStart = Math.Min(selectionStart, selectionStart + selectionLength);
+ selectionLength = 0;
+ }
+ break;
+ case VirtualKey.Down:
+ if (selectionStart != text.Length || selectionLength != 0)
+ {
+ args.Handled = true;
+ if (shift)
+ {
+ selectionLength = text.Length - selectionStart;
+ }
+ else
+ {
+ selectionStart = text.Length;
+ }
+ }
+ break;
+ case VirtualKey.Left:
+ var moveOutLeft = !shift && selectionStart == 0 && selectionLength == 0 || shift && selectionStart + selectionLength == 0;
+ if (!moveOutLeft)
+ {
+ args.Handled = true;
+
+ if (shift)
+ {
+ selectionLength -= 1;
+ }
+ else
+ {
+ if (selectionLength != 0)
+ {
+ selectionStart = Math.Min(selectionStart, selectionStart + selectionLength);
+ }
+ else
+ {
+ selectionStart -= 1;
+ }
+ selectionLength = 0;
+ }
+ }
+ break;
+ case VirtualKey.Right:
+ var moveOutRight = !shift && selectionStart == text.Length && selectionLength == 0 || shift && selectionStart + selectionLength == Text.Length;
+ if (!moveOutRight)
+ {
+ args.Handled = true;
+
+ if (shift)
+ {
+ selectionLength += 1;
+ }
+ else
+ {
+ if (selectionLength != 0)
+ {
+ selectionStart = Math.Max(selectionStart, selectionStart + selectionLength);
+ }
+ else
+ {
+ selectionStart += 1;
+ }
+ selectionLength = 0;
+ }
+ }
+ break;
+ case VirtualKey.Home:
+ args.Handled = true;
+ if (shift)
+ {
+ selectionLength = -selectionStart;
+ }
+ else
+ {
+ selectionStart = 0;
+ selectionLength = 0;
+ }
+ break;
+ case VirtualKey.End:
+ args.Handled = true;
+ if (shift)
+ {
+ selectionLength = text.Length - selectionStart;
+ }
+ else
+ {
+ selectionStart = text.Length;
+ selectionLength = 0;
+ }
+ break;
+ case VirtualKey.Back when !IsReadOnly:
+ args.Handled = true;
+ if (selectionLength != 0)
+ {
+ var start = Math.Min(selectionStart, selectionStart + selectionLength);
+ var end = Math.Max(selectionStart, selectionStart + selectionLength);
+ text = text[..start] + text[end..];
+ selectionLength = 0;
+ selectionStart = start;
+ }
+ else if (selectionStart != 0)
+ {
+ text = text[..(selectionStart - 1)] + text[selectionStart..];
+ selectionStart -= 1;
+ }
+ break;
+ case VirtualKey.Delete when !IsReadOnly:
+ args.Handled = true;
+ if (selectionLength != 0)
+ {
+ var start = Math.Min(selectionStart, selectionStart + selectionLength);
+ var end = Math.Max(selectionStart, selectionStart + selectionLength);
+ text = text[..start] + text[end..];
+ selectionLength = 0;
+ selectionStart = start;
+ }
+ else if (selectionStart != text.Length)
+ {
+ text = text[..selectionStart] + text[(selectionStart + 1)..];
+ selectionStart += 1;
+ }
+ break;
+ case VirtualKey.A when args.KeyboardModifiers.HasFlag(VirtualKeyModifiers.Control):
+ args.Handled = true;
+ selectionStart = 0;
+ selectionLength = text.Length;
+ break;
+ default:
+ var key = (int)args.Key;
+ if (!IsReadOnly && key is >= 'A' and <= 'Z' || args.Key == VirtualKey.Space)
+ {
+ args.Handled = true;
+ var c = args.Key == VirtualKey.Space ? ' ' : shift ? (char)key : char.ToLower((char)key);
+
+
+ var start = Math.Min(selectionStart, selectionStart + selectionLength);
+ var end = Math.Max(selectionStart, selectionStart + selectionLength);
+
+ text = text[..start] + c + text[end..];
+ selectionStart = start + 1;
+ selectionLength = 0;
+ }
+ break;
+ }
+
+ Text = text;
+
+ selectionStart = Math.Max(0, Math.Min(text.Length, selectionStart));
+ selectionLength = Math.Max(-selectionStart, Math.Min(text.Length - selectionStart, selectionLength));
+ SelectInternal(selectionStart, selectionLength);
+ }
+
+ ///
+ /// Takes a possibly-negative selection length, indicating a selection that goes backwards.
+ /// This makes the calculations a lot more natural.
+ ///
+ private void SelectInternal(int selectionStart, int selectionLength)
+ {
+ Select(Math.Min(selectionStart, selectionStart + selectionLength), Math.Abs(selectionLength));
+ _selectionEndsAtTheStart = selectionLength < 0; // set here because Select clears it
+ UpdateScrolling();
+ }
+
+ private void TimerOnTick(object sender, object e)
+ {
+ _showCaret = !_showCaret;
+ UpdateDisplaySelection();
+ }
+
+ protected override void OnPointerMoved(PointerRoutedEventArgs e)
+ {
+ base.OnPointerMoved(e);
+ var displayBlock = TextBoxView.DisplayBlock;
+ var point = e.GetCurrentPoint(displayBlock);
+ var index = displayBlock.Inlines.GetIndexForTextBlock(point.Position - new Point(displayBlock.Padding.Left, displayBlock.Padding.Top));
+ if (point.Properties.IsLeftButtonPressed)
+ {
+ var selectionInternalStart = _selectionEndsAtTheStart ? _selection.start + _selection.length : _selection.start;
+ SelectInternal(selectionInternalStart, index - selectionInternalStart);
+ }
+ }
+
+ partial void OnPointerPressedNative(PointerRoutedEventArgs e)
+ {
+ var displayBlock = TextBoxView.DisplayBlock;
+ var index = displayBlock.Inlines.GetIndexForTextBlock(e.GetCurrentPoint(displayBlock).Position - new Point(displayBlock.Padding.Left, displayBlock.Padding.Top));
+ Select(index, 0);
+ }
}
diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBoxView.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBoxView.skia.cs
index d8e077c3fb8d..ccfea08ecb2f 100644
--- a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBoxView.skia.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBoxView.skia.cs
@@ -7,6 +7,7 @@
using Uno.Foundation.Logging;
using Uno.UI.Xaml.Controls.Extensions;
using Windows.UI.Xaml.Media;
+using Uno.UI;
namespace Windows.UI.Xaml.Controls
{
@@ -25,7 +26,7 @@ public TextBoxView(TextBox textBox)
_textBox = new WeakReference(textBox);
_isPasswordBox = textBox is PasswordBox;
- if (!ApiExtensibility.CreateInstance(this, out _textBoxExtension))
+ if (FeatureConfiguration.TextBox.UseOverlayOnSkia && !ApiExtensibility.CreateInstance(this, out _textBoxExtension))
{
if (this.Log().IsEnabled(LogLevel.Warning))
{
@@ -37,7 +38,7 @@ public TextBoxView(TextBox textBox)
}
public (int start, int length) SelectionBeforeKeyDown =>
- (_textBoxExtension!.GetSelectionStartBeforeKeyDown(), _textBoxExtension.GetSelectionLengthBeforeKeyDown());
+ (_textBoxExtension?.GetSelectionStartBeforeKeyDown() ?? 0, _textBoxExtension?.GetSelectionLengthBeforeKeyDown() ?? 0);
internal IOverlayTextBoxViewExtension? Extension => _textBoxExtension;
@@ -108,6 +109,11 @@ internal void OnSelectionHighlightColorChanged(SolidColorBrush brush)
internal void OnFocusStateChanged(FocusState focusState)
{
+ if (!FeatureConfiguration.TextBox.UseOverlayOnSkia)
+ {
+ return;
+ }
+
if (focusState != FocusState.Unfocused)
{
DisplayBlock.Opacity = 0;
diff --git a/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs b/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs
index 8a675760d603..de2b883ea7ec 100644
--- a/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs
+++ b/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs
@@ -6,6 +6,7 @@
using Windows.Foundation;
using Windows.UI.Composition;
using Windows.UI.Text;
+using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents.TextFormatting;
using Windows.UI.Xaml.Media;
using Uno.UI.Composition;
@@ -16,6 +17,8 @@ namespace Windows.UI.Xaml.Documents
{
partial class InlineCollection
{
+ internal const float CaretThicknessAsRatioOfLineHeight = 0.05f;
+
// This is safe as a static field.
// 1) It's only accessed from UI thread.
// 2) Once we call SKTextBlobBuilder.Build(), the instance is reset to its initial state.
@@ -33,6 +36,11 @@ partial class InlineCollection
private Size _lastDesiredSize;
private Size _lastArrangedSize;
+ // these should only be used by TextBox.
+ internal (int startLine, int startIndex, int endLine, int endIndex)? Selection { get; set; }
+ internal (bool atEndOfSelection, Color color)? Caret { get; set; }
+ internal bool RenderSelectionAndCaret { get; set; }
+
///
/// Measures a block-level inline collection, i.e. one that belongs to a TextBlock (or Paragraph, in the future).
///
@@ -63,7 +71,7 @@ internal Size Measure(Size availableSize, float defaultLineHeight)
bool previousLineWrapped = false;
float availableWidth = wrapping == TextWrapping.NoWrap ? float.PositiveInfinity : (float)availableSize.Width;
- float widestLineWidth = 0;
+ float widestLineWidth = 0, widestLineHeight = 0;
float x = 0;
float height = 0;
@@ -230,18 +238,13 @@ internal Size Measure(Size availableSize, float defaultLineHeight)
}
else
{
- _lastDesiredSize = new Size(widestLineWidth, height);
+ _lastDesiredSize = new Size(widestLineWidth + (RenderSelectionAndCaret && Caret is { } c && c.color != Colors.Transparent ? widestLineHeight * CaretThicknessAsRatioOfLineHeight : 0), height);
}
return _lastDesiredSize;
// Local functions
- static float GetGlyphWidthWithSpacing(GlyphInfo glyph, float characterSpacing)
- {
- return glyph.AdvanceX > 0 ? glyph.AdvanceX + characterSpacing : glyph.AdvanceX;
- }
-
// Gets rendering info for a segment, excluding any trailing spaces.
static (int Length, float Width) GetSegmentRenderInfo(Segment segment, int startGlyph, float characterSpacing)
@@ -269,6 +272,7 @@ void MoveToNextLine(bool currentLineWrapped)
if (x > widestLineWidth)
{
widestLineWidth = x;
+ widestLineHeight = lineHeight;
}
x = 0;
@@ -277,6 +281,11 @@ void MoveToNextLine(bool currentLineWrapped)
}
}
+ private static float GetGlyphWidthWithSpacing(GlyphInfo glyph, float characterSpacing)
+ {
+ return glyph.AdvanceX > 0 ? glyph.AdvanceX + characterSpacing : glyph.AdvanceX;
+ }
+
internal Size Arrange(Size finalSize)
{
_lastArrangedSize = finalSize;
@@ -303,7 +312,7 @@ internal void InvalidateMeasure()
///
/// Renders a block-level inline collection, i.e. one that belongs to a TextBlock (or Paragraph, in the future).
///
- internal void Draw(in DrawingSession session)
+ internal virtual void Draw(in DrawingSession session)
{
if (_renderLines.Count == 0)
{
@@ -325,8 +334,9 @@ internal void Draw(in DrawingSession session)
float y = 0;
- foreach (var line in _renderLines)
+ for (var lineIndex = 0; lineIndex < _renderLines.Count; lineIndex++)
{
+ var line = _renderLines[lineIndex];
// TODO: (Performance) Stop rendering when the lines exceed the available height
(float x, float justifySpaceOffset) = line.GetOffsets((float)_lastArrangedSize.Width, alignment);
@@ -334,10 +344,14 @@ internal void Draw(in DrawingSession session)
y += line.Height;
float baselineOffsetY = line.BaselineOffsetY;
+ var characterCountSoFar = 0;
for (int s = 0; s < line.RenderOrderedSegmentSpans.Count; s++)
{
var segmentSpan = line.RenderOrderedSegmentSpans[s];
+ var currentCharacterCount = segmentSpan.GlyphsLength;
+ characterCountSoFar += currentCharacterCount;
+
if (segmentSpan.GlyphsLength == 0)
{
continue;
@@ -437,9 +451,129 @@ internal void Draw(in DrawingSession session)
}
}
+ {
+ if (RenderSelectionAndCaret && Selection is { } bg && bg.startLine <= lineIndex && lineIndex <= bg.endLine)
+ {
+ var spanStartingIndex = characterCountSoFar - currentCharacterCount;
+
+ float left;
+ if (bg.startLine == lineIndex)
+ {
+ if (bg.startIndex - spanStartingIndex < 0)
+ {
+ if (positions.Length > 0)
+ {
+ left = positions[0].X;
+ }
+ else
+ {
+ // no glyphs, so we're at the start
+ left = x;
+ }
+ }
+ else if (bg.startIndex - spanStartingIndex < positions.Length)
+ {
+ left = positions[bg.startIndex - spanStartingIndex].X;
+ }
+ else if (bg.startIndex - spanStartingIndex < currentCharacterCount) // positions.Length + TrailingSpaces
+ {
+ // x is set to the end of the glyph sequence (no accounting for spaces yet)
+ left = x + justifySpaceOffset * (currentCharacterCount - (bg.startIndex - spanStartingIndex));
+ }
+ else
+ {
+ left = x + justifySpaceOffset * segmentSpan.TrailingSpaces;
+ }
+ }
+ else
+ {
+ if (positions.Length > 0)
+ {
+ left = positions[0].X;
+ }
+ else
+ {
+ // no glyphs, so we're at the start
+ left = x;
+ }
+ }
+
+ float right;
+ if (bg.endLine == lineIndex)
+ {
+ if (bg.endIndex - spanStartingIndex < 0)
+ {
+ if (positions.Length > 0)
+ {
+ right = positions[0].X;
+ }
+ else
+ {
+ // no glyphs, so we're at the start
+ right = x;
+ }
+ }
+ else if (bg.endIndex - spanStartingIndex < positions.Length)
+ {
+ right = positions[bg.endIndex - spanStartingIndex].X;
+ }
+ else if (bg.endIndex - spanStartingIndex < currentCharacterCount) // positions.Length + TrailingSpaces
+ {
+ // x is set to the end of the glyph sequence
+ right = x + justifySpaceOffset * (currentCharacterCount - (bg.endIndex - spanStartingIndex));
+ }
+ else
+ {
+ right = x + justifySpaceOffset * segmentSpan.TrailingSpaces;
+ }
+ }
+ else
+ {
+ right = x + justifySpaceOffset * segmentSpan.TrailingSpaces;
+ }
+
+ canvas.DrawRect(new SKRect(left, 0, right, line.Height), new SKPaint
+ {
+ Color = ((TextBlock)parent).SelectionHighlightColor.Color.ToSKColor(),
+ Style = SKPaintStyle.Fill
+ });
+ }
+ }
+
using var textBlob = _textBlobBuilder.Build();
canvas.DrawText(textBlob, 0, y + baselineOffsetY, paint);
+ {
+ var spanStartingIndex = characterCountSoFar - currentCharacterCount;
+ if (RenderSelectionAndCaret && Caret is { } caret && Selection is { } selection)
+ {
+ var (l, i) = caret.atEndOfSelection ? (selection.endLine, selection.endIndex) : (selection.startLine, selection.startIndex);
+
+ float caretLocation = -1f;
+
+ if (l == lineIndex && i >= spanStartingIndex && i <= characterCountSoFar)
+ {
+ if (i >= spanStartingIndex + positions.Length)
+ {
+ caretLocation = x + justifySpaceOffset * (i - (spanStartingIndex + positions.Length)) - line.Height * 0.05f;
+ }
+ else
+ {
+ caretLocation = positions[i - spanStartingIndex].X;
+ }
+ }
+
+ if (caretLocation != -1f)
+ {
+ canvas.DrawRect(new SKRect(caretLocation, 0, caretLocation + line.Height * 0.05f, line.Height), new SKPaint
+ {
+ Color = new SKColor(caret.color.R, caret.color.G, caret.color.B, caret.color.A),
+ Style = SKPaintStyle.Fill
+ });
+ }
+ }
+ }
+
x += justifySpaceOffset * segmentSpan.TrailingSpaces;
}
}
@@ -453,6 +587,105 @@ static void DrawDecoration(SKCanvas canvas, float x, float y, float width, float
}
}
+ internal int GetIndexForTextBlock(Point p)
+ {
+ var line = GetRenderLineAt(p.Y, true)!;
+
+ if (line is not { })
+ {
+ return 0;
+ }
+
+ var characterCount = 0;
+ foreach (var currentLine in _renderLines.TakeWhile(l => l != line))
+ {
+ foreach (var SegmentSpan in currentLine.SegmentSpans)
+ {
+ characterCount += SegmentSpan.Segment.Glyphs.Count;
+ }
+ }
+
+ var (span, x) = GetRenderSegmentSpanAt(p, true)!.Value;
+
+ foreach (var currentSpan in line.SegmentSpans.TakeWhile(s => !s.Equals(span)))
+ {
+ characterCount += currentSpan.Segment.Glyphs.Count;
+ }
+
+ var segment = span.Segment;
+ var run = (Run)segment.Inline;
+ var characterSpacing = (float)run.FontSize * run.CharacterSpacing / 1000;
+
+ var glyphStart = span.GlyphsStart;
+ var glyphEnd = glyphStart + span.GlyphsLength;
+ for (var i = glyphStart; i < glyphEnd; i++)
+ {
+ var glyph = segment.Glyphs[i];
+ var glyphWidth = GetGlyphWidthWithSpacing(glyph, characterSpacing);
+ if (p.X < x + glyphWidth / 2) // the point is closer to the left side of the glyph.
+ {
+ return characterCount;
+ }
+
+ x += glyphWidth;
+ characterCount++;
+ }
+
+ return characterCount;
+ }
+
+ internal Rect GetRectForTextBlockIndex(int index)
+ {
+ var characterCount = 0;
+ float y = 0, x = 0;
+ var parent = (IBlock)_collection.GetParent();
+
+ for (var lineIndex = 0; lineIndex < _renderLines.Count; lineIndex++)
+ {
+ var line = _renderLines[lineIndex];
+ (x, var justifySpaceOffset) = line.GetOffsets((float)_lastArrangedSize.Width, parent.TextAlignment);
+
+ var spans = line.RenderOrderedSegmentSpans;
+ for (var spanIndex = 0; spanIndex < spans.Count; spanIndex++)
+ {
+ var span = spans[spanIndex];
+ var glyphCount = span.Segment.Glyphs.Count;
+
+ if (index < characterCount + glyphCount)
+ {
+ // we found the right span
+ var segment = span.Segment;
+ var run = (Run)segment.Inline;
+ var characterSpacing = (float)run.FontSize * run.CharacterSpacing / 1000;
+
+ var glyphStart = span.GlyphsStart;
+ var glyphEnd = glyphStart + span.GlyphsLength;
+ for (var i = glyphStart; i < glyphEnd; i++)
+ {
+ var glyph = segment.Glyphs[i];
+ var glyphWidth = GetGlyphWidthWithSpacing(glyph, characterSpacing);
+
+ if (index == characterCount)
+ {
+ return new Rect(x, y, glyphWidth, line.Height);
+ }
+
+ x += glyphWidth;
+ characterCount++;
+ }
+ }
+
+ characterCount += glyphCount;
+ x += span.Width;
+ }
+
+ y += line.Height;
+ }
+
+ // width and height default to 0 if there's nothing there
+ return new Rect(x, y, 0, _renderLines.Count > 0 ? _renderLines[^1].Height : 0);
+ }
+
internal RenderLine? GetRenderLineAt(double y, bool extendedSelection)
{
if (_renderLines.Count == 0)
@@ -478,7 +711,7 @@ static void DrawDecoration(SKCanvas canvas, float x, float y, float width, float
return extendedSelection ? line : null;
}
- internal RenderSegmentSpan? GetRenderSegmentSpanAt(Point point, bool extendedSelection)
+ internal (RenderSegmentSpan span, float x)? GetRenderSegmentSpanAt(Point point, bool extendedSelection)
{
var parent = (IBlock)_collection.GetParent();
@@ -500,13 +733,13 @@ static void DrawDecoration(SKCanvas canvas, float x, float y, float width, float
if (point.X <= spanX && (extendedSelection || point.X >= spanX - span.Width))
{
- return span;
+ return (span, spanX - span.Width);
}
spanX += justifySpaceOffset * span.TrailingSpaces;
} while (i < line.RenderOrderedSegmentSpans.Count);
- return extendedSelection ? span : null;
+ return extendedSelection ? (span, spanX - span.Width) : null;
}
}
}