diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml
index 6929f192c7..0fc78795a6 100644
--- a/samples/Sandbox/MainWindow.axaml
+++ b/samples/Sandbox/MainWindow.axaml
@@ -1,4 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs
index 3d54036d29..7a8b56bbb6 100644
--- a/samples/Sandbox/MainWindow.axaml.cs
+++ b/samples/Sandbox/MainWindow.axaml.cs
@@ -1,7 +1,11 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
using Avalonia.Markup.Xaml;
-using Avalonia.Win32.WinRT.Composition;
+using Avalonia.VisualTree;
namespace Sandbox
{
@@ -11,6 +15,19 @@ namespace Sandbox
{
this.InitializeComponent();
this.AttachDevTools();
+
+ var textBox = this.FindControl("txtBox");
+
+ textBox.TemplateApplied += TextBox_TemplateApplied;
+ }
+
+ private void TextBox_TemplateApplied(object sender, Avalonia.Controls.Primitives.TemplateAppliedEventArgs e)
+ {
+ var textBox = sender as TextBox;
+
+ var textPresenter = e.NameScope.Find("PART_TextPresenter") as TextPresenter;
+
+ DataContext = new TestViewModel(textPresenter);
}
private void InitializeComponent()
@@ -18,4 +35,60 @@ namespace Sandbox
AvaloniaXamlLoader.Load(this);
}
}
+
+ public class TestViewModel : ViewModelBase
+ {
+ private readonly TextPresenter _textPresenter;
+ private double _distance = 45;
+
+ public TestViewModel(TextPresenter textPresenter)
+ {
+ _textPresenter = textPresenter;
+ }
+
+ public double Distance
+ {
+ get => _distance;
+ set
+ {
+ OnDistanceChanged(value);
+ RaisePropertyChanged();
+ }
+ }
+
+ private void OnDistanceChanged(double distance)
+ {
+ if(distance < 0)
+ {
+ distance = 0;
+ }
+
+ if(distance > _textPresenter.TextLayout.Bounds.Width)
+ {
+ distance = _textPresenter.TextLayout.Bounds.Width;
+ }
+
+ var height = _textPresenter.TextLayout.Bounds.Height;
+
+ var distanceY = height / 2;
+
+ _textPresenter.MoveCaretToPoint(new Point(distance, distanceY));
+
+ var caretIndex = _textPresenter.CaretIndex;
+
+ Debug.WriteLine(caretIndex);
+
+ _distance = distance;
+ }
+ }
+
+ public class ViewModelBase : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void RaisePropertyChanged([CallerMemberName]string propertyName = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
}
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index 2a7f3360ad..cae7a8fe75 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -265,7 +265,7 @@ namespace Avalonia.Media
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
- if (GlyphClusters != null)
+ if (GlyphClusters != null && GlyphClusters.Count > 0)
{
if (characterIndex > GlyphClusters[0])
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
index 97df87d3d9..7ab67ea34d 100644
--- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@@ -116,7 +117,30 @@ namespace Avalonia.Media.TextFormatting
length = text.Length;
}
+ length = CoerceLength(text, length);
+
return new ValueSpan(firstTextSourceIndex, length, currentProperties);
}
+
+ private static int CoerceLength(ReadOnlySlice text, int length)
+ {
+ var finalLength = 0;
+
+ var graphemeEnumerator = new GraphemeEnumerator(text);
+
+ while (graphemeEnumerator.MoveNext())
+ {
+ var grapheme = graphemeEnumerator.Current;
+
+ finalLength += grapheme.Text.Length;
+
+ if (finalLength >= length)
+ {
+ return finalLength;
+ }
+ }
+
+ return length;
+ }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index f3c62f4994..0ee791d935 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -183,8 +183,47 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0;
- foreach (var currentRun in _textRuns)
+ for (var i = 0; i < _textRuns.Count; i++)
{
+ var currentRun = _textRuns[i];
+
+ if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ var rightToLeftIndex = i;
+ var rightToLeftDistance = shapedRun.Size.Width;
+
+ while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+ {
+ var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
+
+ if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ break;
+ }
+
+ rightToLeftIndex++;
+
+ rightToLeftDistance += nextShaped.Size.Width;
+ }
+
+ for (var j = rightToLeftIndex; rightToLeftIndex >= 0; j--)
+ {
+ currentRun = _textRuns[j];
+
+ if(distance <= currentDistance + rightToLeftDistance - currentRun.Size.Width)
+ {
+ currentPosition += currentRun.TextSourceLength;
+ rightToLeftDistance -= currentRun.Size.Width;
+
+ continue;
+ }
+
+ characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
+
+ break;
+ }
+ }
+
if (currentDistance + currentRun.Size.Width < distance)
{
currentDistance += currentRun.Size.Width;
@@ -255,6 +294,37 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = _textRuns[index];
+ if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ var i = index;
+
+ while (i + 1 <= _textRuns.Count - 1)
+ {
+ var nextRun = _textRuns[i + 1];
+
+ if (nextRun is ShapedTextCharacters nextShapedRun)
+ {
+ if (nextShapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ break;
+ }
+ }
+
+ i++;
+ }
+
+ while (i > index)
+ {
+ var rightToLeftRun = _textRuns[i];
+
+ currentPosition += rightToLeftRun.TextSourceLength;
+
+ remainingLength -= rightToLeftRun.TextSourceLength;
+
+ i--;
+ }
+ }
+
if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
flowDirection, out var distance, out _))
{
@@ -442,92 +512,130 @@ namespace Avalonia.Media.TextFormatting
continue;
}
- if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
-
- currentPosition += currentRun.TextSourceLength;
-
- continue;
- }
-
var characterLength = 0;
var endX = startX;
+ var runWidth = 0.0;
+ TextRunBounds? currentRunBounds = null;
- if (currentRun is ShapedTextCharacters currentShapedRun)
+ var currentShapedRun = currentRun as ShapedTextCharacters;
+
+ if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
{
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+ var rightToLeftIndex = index;
+ startX += currentShapedRun.Size.Width;
- currentPosition += offset;
+ while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+ {
+ var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
- var startIndex = currentRun.Text.Start + offset;
+ if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ break;
+ }
- double startOffset;
- double endOffset;
+ startX += nextShapedRun.Size.Width;
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ rightToLeftIndex++;
+ }
+
+ if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds))
{
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ startX = currentRunBounds!.Rectangle.Left;
+ endX = currentRunBounds.Rectangle.Right;
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ runWidth = currentRunBounds.Rectangle.Width;
}
- else
+
+ currentDirection = FlowDirection.RightToLeft;
+ }
+ else
+ {
+ if (currentShapedRun != null)
{
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+ {
+ startX += currentRun.Size.Width;
- if (currentPosition < startIndex)
+ currentPosition += currentRun.TextSourceLength;
+
+ continue;
+ }
+
+ var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+
+ currentPosition += offset;
+
+ var startIndex = currentRun.Text.Start + offset;
+
+ double startOffset;
+ double endOffset;
+
+ if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
- startOffset = endOffset;
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
else
{
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+ if (currentPosition < startIndex)
+ {
+ startOffset = endOffset;
+ }
+ else
+ {
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ }
}
- }
- startX += startOffset;
+ startX += startOffset;
- endX += endOffset;
+ endX += endOffset;
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+ var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
- characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+ characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
- currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
- FlowDirection.LeftToRight :
- FlowDirection.RightToLeft;
- }
- else
- {
- if (currentPosition < firstTextSourceIndex)
+ currentDirection = FlowDirection.LeftToRight;
+ }
+ else
{
- startX += currentRun.Size.Width;
+ if (currentPosition < firstTextSourceIndex)
+ {
+ startX += currentRun.Size.Width;
+ }
+
+ if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+ {
+ endX += currentRun.Size.Width;
+
+ characterLength = currentRun.TextSourceLength;
+ }
}
- if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+ if (endX < startX)
{
- endX += currentRun.Size.Width;
+ (endX, startX) = (startX, endX);
+ }
- characterLength = currentRun.TextSourceLength;
+ //Lines that only contain a linebreak need to be covered here
+ if (characterLength == 0)
+ {
+ characterLength = NewLineLength;
}
- }
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
+ runWidth = endX - startX;
+ currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
- //Lines that only contain a linebreak need to be covered here
- if (characterLength == 0)
- {
- characterLength = NewLineLength;
- }
+ currentPosition += characterLength;
- var runWidth = endX - startX;
- var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+ remainingLength -= characterLength;
+ }
- if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
+ if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
@@ -537,32 +645,26 @@ namespace Avalonia.Media.TextFormatting
textBounds.Rectangle = currentRect;
- textBounds.TextRunBounds.Add(currentRunBounds);
+ textBounds.TextRunBounds.Add(currentRunBounds!);
}
else
{
- currentRect = currentRunBounds.Rectangle;
+ currentRect = currentRunBounds!.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
}
}
currentWidth += runWidth;
- currentPosition += characterLength;
+
- if (currentPosition > characterIndex)
+ if (remainingLength <= 0 || currentPosition >= characterIndex)
{
break;
}
startX = endX;
lastDirection = currentDirection;
- remainingLength -= characterLength;
-
- if (remainingLength <= 0)
- {
- break;
- }
}
return result;
@@ -674,7 +776,7 @@ namespace Avalonia.Media.TextFormatting
var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
- if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
+ if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
{
@@ -692,7 +794,7 @@ namespace Avalonia.Media.TextFormatting
result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
}
- }
+ }
currentWidth += runWidth;
currentPosition += characterLength;
@@ -716,6 +818,107 @@ namespace Avalonia.Media.TextFormatting
return result;
}
+ private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds)
+ {
+ textRunBounds = null;
+
+ for (var index = runIndex; index >= 0; index--)
+ {
+ if (TextRuns[index] is not DrawableTextRun currentRun)
+ {
+ continue;
+ }
+
+ if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+ {
+ startX -= currentRun.Size.Width;
+
+ currentPosition += currentRun.TextSourceLength;
+
+ continue;
+ }
+
+ var characterLength = 0;
+ var endX = startX;
+
+ if (currentRun is ShapedTextCharacters currentShapedRun)
+ {
+ var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+
+ currentPosition += offset;
+
+ var startIndex = currentRun.Text.Start + offset;
+ double startOffset;
+ double endOffset;
+
+ if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (currentPosition < startIndex)
+ {
+ startOffset = endOffset = 0;
+ }
+ else
+ {
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ }
+ }
+ else
+ {
+ endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+ startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ }
+
+ startX -= currentRun.Size.Width - startOffset;
+ endX -= currentRun.Size.Width - endOffset;
+
+ var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+ var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+ characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+ }
+ else
+ {
+ if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+ {
+ endX -= currentRun.Size.Width;
+ }
+
+ if (currentPosition < firstTextSourceIndex)
+ {
+ startX -= currentRun.Size.Width;
+
+ characterLength = currentRun.TextSourceLength;
+ }
+ }
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ //Lines that only contain a linebreak need to be covered here
+ if (characterLength == 0)
+ {
+ characterLength = NewLineLength;
+ }
+
+ var runWidth = endX - startX;
+
+ remainingLength -= characterLength;
+
+ currentPosition += characterLength;
+
+ textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+
+ return true;
+ }
+
+ return false;
+ }
+
public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength)
{
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
@@ -1319,12 +1522,12 @@ namespace Avalonia.Media.TextFormatting
case TextAlignment.Center:
var start = (_paragraphWidth - width) / 2;
- if(paragraphFlowDirection == FlowDirection.RightToLeft)
+ if (paragraphFlowDirection == FlowDirection.RightToLeft)
{
start -= (widthIncludingTrailingWhitespace - width);
}
- return Math.Max(0, start);
+ return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index e463bc5731..4f9be86641 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -9,6 +9,7 @@ using Avalonia.VisualTree;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
using Avalonia.Controls.Documents;
+using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls.Presenters
{
@@ -496,14 +497,14 @@ namespace Avalonia.Controls.Presenters
var length = Math.Max(selectionStart, selectionEnd) - start;
IReadOnlyList>? textStyleOverrides = null;
-
+
if (length > 0)
{
textStyleOverrides = new[]
{
new ValueSpan(start, length,
new GenericTextRunProperties(typeface, FontSize,
- foregroundBrush: SelectionForegroundBrush ?? Brushes.White))
+ foregroundBrush: SelectionForegroundBrush ?? Brushes.Red))
};
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 1b268db2f7..8490bfd3a0 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -17,6 +17,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
+using System.Diagnostics;
namespace Avalonia.Controls
{
@@ -1240,9 +1241,12 @@ namespace Avalonia.Controls
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
- _presenter.MoveCaretToPoint(point);
+ var hit = _presenter.TextLayout.HitTestPoint(point);
+
+ var caretIndex = hit.TextPosition;
+
+ Debug.WriteLine($"TextPos: {caretIndex}, X: {point.X}");
- var caretIndex = _presenter.CaretIndex;
var text = Text;
if (text != null && _wordSelectionStart >= 0)
@@ -1266,7 +1270,7 @@ namespace Avalonia.Controls
}
else
{
- SelectionEnd = _presenter.CaretIndex;
+ SelectionEnd = caretIndex;
}
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index 7d33f094fa..6d057d900e 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
@@ -914,14 +915,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public void Should_Get_CharacterHit_From_Distance_RTL()
{
using (Start())
- {
+ {
var text = "أَبْجَدِيَّة عَرَبِيَّة";
var layout = new TextLayout(
- text,
- Typeface.Default,
- 12,
- Brushes.Black);
+ text,
+ Typeface.Default,
+ 12,
+ Brushes.Black);
var textLine = layout.TextLines[0];
@@ -952,6 +953,63 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
rect = layout.HitTestTextPosition(23);
Assert.Equal(0, rect.Left, 5);
+
+ }
+ }
+
+ [Fact]
+ public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles()
+ {
+ using (Start())
+ {
+ var text = "أَبْجَدِيَّة عَرَبِيَّة";
+
+ var i = 0;
+
+ var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory());
+
+ while (graphemeEnumerator.MoveNext())
+ {
+ var grapheme = graphemeEnumerator.Current;
+
+ var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
+
+ i += grapheme.Text.Length;
+
+ var layout = new TextLayout(
+ text,
+ Typeface.Default,
+ 12,
+ Brushes.Black,
+ textStyleOverrides: textStyleOverrides);
+
+ var textLine = layout.TextLines[0];
+
+ var shapedRuns = textLine.TextRuns.Cast().ToList();
+
+ var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList();
+
+ var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList();
+
+ var currentX = 0.0;
+
+ for (int j = 0; j < clusters.Count; j++)
+ {
+ var cluster = clusters[j];
+
+ var characterHit = textLine.GetCharacterHitFromDistance(currentX);
+
+ Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+
+ var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
+
+ Assert.Equal(currentX, distance);
+
+ var glyphAdvance = glyphAdvances[j];
+
+ currentX += glyphAdvance;
+ }
+ }
}
}