diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs
index 00a393cf61..00f9b918cb 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs
@@ -22,9 +22,7 @@ namespace Avalonia.Media.TextFormatting
GlyphRun = glyphRun;
}
- ///
- /// Gets the bounds.
- ///
+ ///
public override Rect Bounds => GlyphRun.Bounds;
///
@@ -35,11 +33,7 @@ namespace Avalonia.Media.TextFormatting
///
public GlyphRun GlyphRun { get; }
- ///
- /// Draws the at the given origin.
- ///
- /// The drawing context.
- /// The origin.
+ ///
public override void Draw(IDrawingContextImpl drawingContext, Point origin)
{
if (GlyphRun.GlyphIndices.Length == 0)
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
index 66071878fd..e84242c628 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
@@ -166,7 +166,7 @@ namespace Avalonia.Media.TextFormatting
var splitResult = SplitTextRuns(textRuns, measuredLength);
- var trimmedRuns = new List(splitResult.First.Count + 1);
+ var trimmedRuns = new List(splitResult.First.Count + 1);
trimmedRuns.AddRange(splitResult.First);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs
index ab93848c23..11d241bc34 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs
@@ -6,42 +6,43 @@ namespace Avalonia.Media.TextFormatting
{
internal class SimpleTextLine : TextLine
{
- public SimpleTextLine(TextPointer textPointer, IReadOnlyList textRuns, TextLineMetrics lineMetrics) :
- base(textPointer, textRuns, lineMetrics)
- {
+ private readonly IReadOnlyList _textRuns;
+ public SimpleTextLine(TextPointer textPointer, IReadOnlyList textRuns, TextLineMetrics lineMetrics)
+ {
+ Text = textPointer;
+ _textRuns = textRuns;
+ LineMetrics = lineMetrics;
}
+ ///
+ public override TextPointer Text { get; }
+
+ ///
+ public override IReadOnlyList TextRuns => _textRuns;
+
+ ///
+ public override TextLineMetrics LineMetrics { get; }
+
+ ///
public override void Draw(IDrawingContextImpl drawingContext, Point origin)
{
var currentX = origin.X;
- foreach (var textRun in TextRuns)
+ foreach (var textRun in _textRuns)
{
- if (!(textRun is DrawableTextRun drawableRun))
- {
- continue;
- }
-
var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X,
origin.Y + LineMetrics.BaselineOrigin.Y);
- drawableRun.Draw(drawingContext, baselineOrigin);
+ textRun.Draw(drawingContext, baselineOrigin);
- currentX += drawableRun.Bounds.Width;
+ currentX += textRun.Bounds.Width;
}
}
- ///
- /// Client to get the character hit corresponding to the specified
- /// distance from the beginning of the line.
- ///
- /// distance in text flow direction from the beginning of the line
- /// character hit
+ ///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
- var first = Text.Start;
-
if (distance < 0)
{
// hit happens before the line, return the first position
@@ -49,45 +50,30 @@ namespace Avalonia.Media.TextFormatting
}
// process hit that happens within the line
- var runIndex = new CharacterHit();
+ var characterHit = new CharacterHit();
- foreach (var run in TextRuns)
+ foreach (var run in _textRuns)
{
- var shapedTextRun = (ShapedTextRun)run;
-
- first += runIndex.TrailingLength;
+ characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
- runIndex = shapedTextRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
-
- first += runIndex.FirstCharacterIndex;
-
- if (distance <= shapedTextRun.Bounds.Width)
+ if (distance <= run.Bounds.Width)
{
break;
}
- distance -= shapedTextRun.Bounds.Width;
+ distance -= run.Bounds.Width;
}
- return new CharacterHit(first, runIndex.TrailingLength);
+ return characterHit;
}
- ///
- /// Client to get the distance from the beginning of the line from the specified
- /// character hit.
- ///
- /// character hit of the character to query the distance.
- /// distance in text flow direction from the beginning of the line.
+ ///
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
- return DistanceFromCp(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
+ return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
}
- ///
- /// Client to get the next character hit for caret navigation
- ///
- /// the current character hit
- /// the next character hit
+ ///
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
int nextVisibleCp;
@@ -95,7 +81,7 @@ namespace Avalonia.Media.TextFormatting
if (characterHit.TrailingLength == 0)
{
- navigableCpFound = FindNextVisibleCp(characterHit.FirstCharacterIndex, out nextVisibleCp);
+ navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex, out nextVisibleCp);
if (navigableCpFound)
{
@@ -104,7 +90,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- navigableCpFound = FindNextVisibleCp(characterHit.FirstCharacterIndex + 1, out nextVisibleCp);
+ navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex + 1, out nextVisibleCp);
if (navigableCpFound)
{
@@ -116,18 +102,14 @@ namespace Avalonia.Media.TextFormatting
return characterHit;
}
- ///
- /// Client to get the previous character hit for caret navigation
- ///
- /// the current character hit
- /// the previous character hit
+ ///
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
- int previousVisibleCp;
- bool navigableCpFound;
+ int previousCodepointIndex;
+ bool codepointIndexFound;
- int cpHit = characterHit.FirstCharacterIndex;
- bool trailingHit = (characterHit.TrailingLength != 0);
+ var cpHit = characterHit.FirstCharacterIndex;
+ var trailingHit = characterHit.TrailingLength != 0;
// Input can be right after the end of the current line. Snap it to be at the end of the line.
if (cpHit >= Text.Start + Text.Length)
@@ -139,32 +121,28 @@ namespace Avalonia.Media.TextFormatting
if (trailingHit)
{
- navigableCpFound = FindPreviousVisibleCp(cpHit, out previousVisibleCp);
+ codepointIndexFound = FindPreviousCodepointIndex(cpHit, out previousCodepointIndex);
- if (navigableCpFound)
+ if (codepointIndexFound)
{
// Move from trailing to leading edge
- return new CharacterHit(previousVisibleCp, 0);
+ return new CharacterHit(previousCodepointIndex, 0);
}
}
- navigableCpFound = FindPreviousVisibleCp(cpHit - 1, out previousVisibleCp);
+ codepointIndexFound = FindPreviousCodepointIndex(cpHit - 1, out previousCodepointIndex);
- if (navigableCpFound)
+ if (codepointIndexFound)
{
// Move from leading edge of current character to leading edge of previous
- return new CharacterHit(previousVisibleCp, 0);
+ return new CharacterHit(previousCodepointIndex, 0);
}
// Can't move, we're before the first character
return characterHit;
}
- ///
- /// Client to get the previous character hit after backspacing
- ///
- /// the current character hit
- /// the character hit after backspacing
+ ///
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
{
// same operation as move-to-previous
@@ -172,44 +150,41 @@ namespace Avalonia.Media.TextFormatting
}
///
- /// Get distance from line start to the specified cp
+ /// Get distance from line start to the specified codepoint index
///
- private double DistanceFromCp(int currentIndex)
+ private double DistanceFromCodepointIndex(int codepointIndex)
{
- var distance = 0.0;
- var dcp = currentIndex - Text.Start;
+ var currentDistance = 0.0;
- foreach (var textRun in TextRuns)
+ foreach (var textRun in _textRuns)
{
- var run = (ShapedTextRun)textRun;
-
- distance += run.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(dcp));
-
- if (dcp <= run.Text.Length)
+ if (codepointIndex > textRun.Text.End)
{
- break;
+ currentDistance += textRun.Bounds.Width;
+
+ continue;
}
- dcp -= run.Text.Length;
+ return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex));
}
- return distance;
+ return currentDistance;
}
///
- /// Search forward from the given cp index (inclusive) to find the next navigable cp index.
- /// Return true if one such cp is found, false otherwise.
+ /// Search forward from the given codepoint index (inclusive) to find the next navigable codepoint index.
+ /// Return true if one such codepoint index is found, false otherwise.
///
- private bool FindNextVisibleCp(int cp, out int cpVisible)
+ private bool FindNextCodepointIndex(int codepointIndex, out int nextCodepointIndex)
{
- cpVisible = cp;
+ nextCodepointIndex = codepointIndex;
- if (cp >= Text.Start + Text.Length)
+ if (codepointIndex >= Text.Start + Text.Length)
{
return false; // Cannot go forward anymore
}
- GetRunIndexAtCp(cp, out var runIndex, out var cpRunStart);
+ GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var cpRunStart);
while (runIndex < TextRuns.Count)
{
@@ -217,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
// navigable.
if (runIndex < TextRuns.Count)
{
- cpVisible = Math.Max(cpRunStart, cp);
+ nextCodepointIndex = Math.Max(cpRunStart, codepointIndex);
return true;
}
@@ -228,29 +203,29 @@ namespace Avalonia.Media.TextFormatting
}
///
- /// Search backward from the given cp index (inclusive) to find the previous navigable cp index.
- /// Return true if one such cp is found, false otherwise.
+ /// Search backward from the given codepoint index (inclusive) to find the previous navigable codepoint index.
+ /// Return true if one such codepoint is found, false otherwise.
///
- private bool FindPreviousVisibleCp(int cp, out int cpVisible)
+ private bool FindPreviousCodepointIndex(int codepointIndex, out int previousCodepointIndex)
{
- cpVisible = cp;
+ previousCodepointIndex = codepointIndex;
- if (cp < Text.Start)
+ if (codepointIndex < Text.Start)
{
return false; // Cannot go backward anymore.
}
// Position the cpRunEnd at the end of the span that contains the given cp
- GetRunIndexAtCp(cp, out var runIndex, out var cpRunEnd);
+ GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var codepointIndexAtRunEnd);
- cpRunEnd += TextRuns[runIndex].Text.End;
+ codepointIndexAtRunEnd += TextRuns[runIndex].Text.End;
while (runIndex >= 0)
{
// Visible content has caret stops at its leading edge.
if (runIndex + 1 < TextRuns.Count)
{
- cpVisible = Math.Min(cpRunEnd, cp);
+ previousCodepointIndex = Math.Min(codepointIndexAtRunEnd, codepointIndex);
return true;
}
@@ -258,25 +233,26 @@ namespace Avalonia.Media.TextFormatting
if (runIndex == TextRuns.Count)
{
// Get the cp index at the beginning of the newline sequence.
- cpVisible = cpRunEnd - TextRuns[runIndex].Text.Length + 1;
+ previousCodepointIndex = codepointIndexAtRunEnd - TextRuns[runIndex].Text.Length + 1;
return true;
}
- cpRunEnd -= TextRuns[runIndex--].Text.Length;
+ codepointIndexAtRunEnd -= TextRuns[runIndex--].Text.Length;
}
return false;
}
- private void GetRunIndexAtCp(int cp, out int runIndex, out int cpRunStart)
+ private void GetRunIndexAtCodepointIndex(int codepointIndex, out int runIndex, out int codepointIndexAtRunStart)
{
- cpRunStart = Text.Start;
+ codepointIndexAtRunStart = Text.Start;
runIndex = 0;
// Find the span that contains the given cp
- while (runIndex < TextRuns.Count && cpRunStart + TextRuns[runIndex].Text.Length <= cp)
+ while (runIndex < TextRuns.Count &&
+ codepointIndexAtRunStart + TextRuns[runIndex].Text.Length <= codepointIndex)
{
- cpRunStart += TextRuns[runIndex++].Text.Length;
+ codepointIndexAtRunStart += TextRuns[runIndex++].Text.Length;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index dc3942f224..0c9013e6f7 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Media.TextFormatting
private readonly ReadOnlySlice _text;
private readonly TextParagraphProperties _paragraphProperties;
- private readonly TextStyleRun[] _textStyleOverrides;
+ private readonly IReadOnlyList _textStyleOverrides;
///
/// Initializes a new instance of the class.
@@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting
TextDecorationCollection textDecorations = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
- TextStyleRun[] textStyleOverrides = null)
+ IReadOnlyList textStyleOverrides = null)
{
_text = string.IsNullOrEmpty(text) ?
new ReadOnlySlice() :
@@ -276,10 +276,10 @@ namespace Avalonia.Media.TextFormatting
{
private readonly ReadOnlySlice _text;
private readonly TextStyle _defaultStyle;
- private readonly TextStyleRun[] _textStyleOverrides;
+ private readonly IReadOnlyList _textStyleOverrides;
public FormattedTextSource(ReadOnlySlice text, TextStyle defaultStyle,
- TextStyleRun[] textStyleOverrides)
+ IReadOnlyList textStyleOverrides)
{
_text = text;
_defaultStyle = defaultStyle;
@@ -311,8 +311,13 @@ namespace Avalonia.Media.TextFormatting
/// The created text style run.
///
private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice text,
- TextStyle defaultTextStyle, ReadOnlySpan textStyleOverrides)
+ TextStyle defaultTextStyle, IReadOnlyList textStyleOverrides)
{
+ if(textStyleOverrides == null || textStyleOverrides.Count == 0)
+ {
+ return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle);
+ }
+
var currentTextStyle = defaultTextStyle;
var hasOverride = false;
@@ -321,7 +326,7 @@ namespace Avalonia.Media.TextFormatting
var length = 0;
- for (; i < textStyleOverrides.Length; i++)
+ for (; i < textStyleOverrides.Count; i++)
{
var styleOverride = textStyleOverrides[i];
@@ -341,7 +346,7 @@ namespace Avalonia.Media.TextFormatting
if (textPointer.Start > text.Start)
{
if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat ||
- currentTextStyle.Foreground != styleOverride.Style.Foreground)
+ !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground))
{
length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length);
@@ -361,9 +366,9 @@ namespace Avalonia.Media.TextFormatting
currentTextStyle = styleOverride.Style;
}
- if (length < text.Length && i == textStyleOverrides.Length)
+ if (length < text.Length && i == textStyleOverrides.Count)
{
- if (currentTextStyle.Foreground == defaultTextStyle.Foreground &&
+ if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) &&
currentTextStyle.TextFormat == defaultTextStyle.TextFormat)
{
length = text.Length;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 27f5355987..296aef0fed 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -11,25 +11,13 @@ namespace Avalonia.Media.TextFormatting
///
public abstract class TextLine
{
- protected TextLine()
- {
-
- }
-
- protected TextLine(TextPointer text, IReadOnlyList textRuns, TextLineMetrics lineMetrics)
- {
- Text = text;
- TextRuns = textRuns;
- LineMetrics = lineMetrics;
- }
-
///
/// Gets the text.
///
///
/// The text pointer.
///
- public TextPointer Text { get; protected set; }
+ public abstract TextPointer Text { get; }
///
/// Gets the text runs.
@@ -37,7 +25,7 @@ namespace Avalonia.Media.TextFormatting
///
/// The text runs.
///
- public IReadOnlyList TextRuns { get; protected set; }
+ public abstract IReadOnlyList TextRuns { get; }
///
/// Gets the line metrics.
@@ -45,7 +33,7 @@ namespace Avalonia.Media.TextFormatting
///
/// The line metrics.
///
- public TextLineMetrics LineMetrics { get; protected set; }
+ public abstract TextLineMetrics LineMetrics { get; }
///
/// Draws the at the given origin.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
index 48e3312906..eb3a4129bc 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
@@ -43,12 +43,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- ///
- /// Shapes the specified text and returns a resulting glyph run.
- ///
- /// The text.
- /// The text format.
- /// A shaped glyph run.
+ ///
public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat)
{
return _platformImpl.ShapeText(text, textFormat);
diff --git a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs
index 63cb7c2f36..f424027910 100644
--- a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs
@@ -43,7 +43,7 @@ namespace Avalonia.Skia.UnitTests
{
var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black);
- var textSource = new MultipleBufferTextSource(defaultTextRunStyle);
+ var textSource = new MultiBufferTextSource(defaultTextRunStyle);
var formatter = new SimpleTextFormatter();
@@ -56,18 +56,20 @@ namespace Avalonia.Skia.UnitTests
}
}
- private class MultipleBufferTextSource : ITextSource
+ private class MultiBufferTextSource : ITextSource
{
private readonly string[] _runTexts;
private readonly TextStyle _defaultStyle;
- public MultipleBufferTextSource(TextStyle defaultStyle)
+ public MultiBufferTextSource(TextStyle defaultStyle)
{
_defaultStyle = defaultStyle;
_runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" };
}
+ public TextPointer TextPointer => new TextPointer(0, 50);
+
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex == 50)
@@ -197,7 +199,7 @@ namespace Avalonia.Skia.UnitTests
}
[Fact]
- public void Should_Split_Run_On_Direction()
+ public void Should_Split_Run_On_Script()
{
using (Start())
{
@@ -219,18 +221,36 @@ namespace Avalonia.Skia.UnitTests
{
using (Start())
{
- const string text = "0123456789";
-
- var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default));
+ var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default));
var formatter = new SimpleTextFormatter();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
- var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(text.Length));
+ var currentDistance = 0.0;
+
+ foreach (var run in textLine.TextRuns)
+ {
+ var textRun = (ShapedTextRun)run;
+
+ var glyphRun = textRun.GlyphRun;
- Assert.Equal(textLine.LineMetrics.Size.Width, distance);
+ for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
+ {
+ var cluster = glyphRun.GlyphClusters[i];
+
+ var advance = glyphRun.GlyphAdvances[i];
+
+ var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
+
+ Assert.Equal(currentDistance, distance);
+
+ currentDistance += advance;
+ }
+ }
+
+ Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(textSource.TextPointer.Length)));
}
}
@@ -239,18 +259,40 @@ namespace Avalonia.Skia.UnitTests
{
using (Start())
{
- const string text = "0123456789";
-
- var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default));
+ var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default));
var formatter = new SimpleTextFormatter();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties());
- var characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width);
+ var currentDistance = 0.0;
+
+ CharacterHit characterHit;
+
+ foreach (var run in textLine.TextRuns)
+ {
+ var textRun = (ShapedTextRun)run;
+
+ var glyphRun = textRun.GlyphRun;
+
+ for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
+ {
+ var cluster = glyphRun.GlyphClusters[i];
+
+ var advance = glyphRun.GlyphAdvances[i];
+
+ characterHit = textLine.GetCharacterHitFromDistance(currentDistance);
+
+ Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+
+ currentDistance += advance;
+ }
+ }
+
+ characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width);
- Assert.Equal(textLine.Text.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+ Assert.Equal(textSource.TextPointer.End, characterHit.FirstCharacterIndex);
}
}