diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index 7a5e6ce426..c42c6f100c 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -67,7 +67,7 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey)
{
- var rect = HitTestTextPosition(underscore);
+ var rect = TextLayout.HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5);
context.DrawLine(
new Pen(Foreground, 1),
@@ -76,80 +76,6 @@ namespace Avalonia.Controls.Primitives
}
}
- ///
- /// Get the pixel location relative to the top-left of the layout box given the text position.
- ///
- /// The text position.
- ///
- private Rect HitTestTextPosition(int textPosition)
- {
- if (TextLayout == null)
- {
- return new Rect();
- }
-
- if (TextLayout.TextLines.Count == 0)
- {
- return new Rect();
- }
-
- if (textPosition < 0 || textPosition >= Text.Length)
- {
- var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
-
- var lineX = lastLine.LineMetrics.Size.Width;
-
- var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
-
- return new Rect(lineX, lineY, 0, lastLine.LineMetrics.Size.Height);
- }
-
- var currentY = 0.0;
-
- foreach (var textLine in TextLayout.TextLines)
- {
- if (textLine.TextRange.End < textPosition)
- {
- currentY += textLine.LineMetrics.Size.Height;
-
- continue;
- }
-
- var currentX = 0.0;
-
- foreach (var textRun in textLine.TextRuns)
- {
- if (!(textRun is ShapedTextCharacters shapedTextCharacters))
- {
- continue;
- }
-
- if (shapedTextCharacters.GlyphRun.Characters.End < textPosition)
- {
- currentX += shapedTextCharacters.Size.Width;
-
- continue;
- }
-
- var characterHit =
- shapedTextCharacters.GlyphRun.FindNearestCharacterHit(textPosition, out var width);
-
- var distance = shapedTextCharacters.GlyphRun.GetDistanceFromCharacterHit(characterHit);
-
- currentX += distance - width;
-
- if (characterHit.TrailingLength == 0)
- {
- width = 0.0;
- }
-
- return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height);
- }
- }
-
- return new Rect();
- }
-
///
protected override TextLayout CreateTextLayout(Size constraint, string text)
{
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 31517ba59d..14cde774f4 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -416,26 +416,8 @@ namespace Avalonia.Controls
{
return;
}
-
- var textAlignment = TextAlignment;
-
- var width = Bounds.Size.Width;
-
- var offsetX = 0.0;
-
- switch (textAlignment)
- {
- case TextAlignment.Center:
- offsetX = (width - TextLayout.Size.Width) / 2;
- break;
-
- case TextAlignment.Right:
- offsetX = width - TextLayout.Size.Width;
- break;
- }
-
+
var padding = Padding;
-
var top = padding.Top;
var textSize = TextLayout.Size;
@@ -453,10 +435,7 @@ namespace Avalonia.Controls
}
}
- using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, top)))
- {
- TextLayout.Draw(context);
- }
+ TextLayout.Draw(context, new Point(padding.Left, top));
}
///
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 62cac378d7..addc89b205 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -90,9 +90,8 @@ namespace Avalonia.Headless
return new HeadlessBitmapStub(destinationSize, new Vector(96, 96));
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- width = 100;
return new HeadlessGlyphRunStub();
}
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index 5c63546f5d..6e937b7e13 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/src/Avalonia.Input/MouseDevice.cs
@@ -435,7 +435,7 @@ namespace Avalonia.Input
IInputElement? branch = null;
- var el = element;
+ IInputElement? el = element;
while (el != null)
{
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index 805d1955ea..35ba8f2b19 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -1,4 +1,58 @@
Compat issues with assembly Avalonia.Visuals:
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
+CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextFormatting.TextRunProperties Avalonia.Media.TextFormatting.GenericTextParagraphProperties.DefaultTextRunProperties' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFormatting.GenericTextParagraphProperties.LineHeight' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextFormatting.TextRunProperties Avalonia.Media.TextFormatting.GenericTextParagraphProperties.DefaultTextRunProperties.get()' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFormatting.GenericTextParagraphProperties.LineHeight.get()' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment.get()' is non-virtual in the implementation but is virtual in the contract.
+CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping.get()' is non-virtual in the implementation but is virtual in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.GenericTextRunProperties..ctor(Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextDecorationCollection, Avalonia.Media.IBrush, Avalonia.Media.IBrush, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLine.LineMetrics.get()' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLineMetrics.TextRange.get()' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract.
@@ -6,4 +60,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
-Total Issues: 7
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 64
diff --git a/src/Avalonia.Visuals/Media/BaselineAlignment.cs b/src/Avalonia.Visuals/Media/BaselineAlignment.cs
new file mode 100644
index 0000000000..71759003fa
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/BaselineAlignment.cs
@@ -0,0 +1,32 @@
+namespace Avalonia.Media
+{
+ ///
+ /// Enum specifying where a box should be positioned Vertically
+ ///
+ public enum BaselineAlignment
+ {
+ /// Align top toward top of container
+ Top,
+
+ /// Center vertically
+ Center,
+
+ /// Align bottom toward bottom of container
+ Bottom,
+
+ /// Align at baseline
+ Baseline,
+
+ /// Align toward text's top of container
+ TextTop,
+
+ /// Align toward text's bottom of container
+ TextBottom,
+
+ /// Align baseline to subscript position of container
+ Subscript,
+
+ /// Align baseline to superscript position of container
+ Superscript,
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/FlowDirection.cs b/src/Avalonia.Visuals/Media/FlowDirection.cs
new file mode 100644
index 0000000000..45684907b1
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/FlowDirection.cs
@@ -0,0 +1,25 @@
+namespace Avalonia.Media
+{
+ ///
+ /// The 'flow-direction' property specifies whether the primary text advance
+ /// direction shall be left-to-right or right-to-left.
+ ///
+ public enum FlowDirection
+ {
+ ///
+ /// Sets the primary text advance direction to left-to-right, and the line
+ /// progression direction to top-to-bottom as is common in most Roman-based
+ /// documents. For most characters, the current text position is advanced
+ /// from left to right after each glyph is rendered. The 'direction' property
+ /// is set to 'ltr'.
+ ///
+ LeftToRight,
+
+ ///
+ /// Sets the primary text advance direction to right-to-left, and the line
+ /// progression direction to top-to-bottom as is common in Arabic or Hebrew
+ /// scripts. The direction property is set to 'rtl'.
+ ///
+ RightToLeft
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
index af228ec57b..2b787462e4 100644
--- a/src/Avalonia.Visuals/Media/GlyphRun.cs
+++ b/src/Avalonia.Visuals/Media/GlyphRun.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@@ -16,9 +17,9 @@ namespace Avalonia.Media
private IGlyphRunImpl _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
- private Size? _size;
private int _biDiLevel;
private Point? _baselineOrigin;
+ private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice _glyphIndices;
private ReadOnlySlice _glyphAdvances;
@@ -90,6 +91,24 @@ namespace Avalonia.Media
set => Set(ref _fontRenderingEmSize, value);
}
+ ///
+ /// Gets or sets the conservative bounding box of the .
+ ///
+ public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);
+
+ ///
+ ///
+ ///
+ public GlyphRunMetrics Metrics
+ {
+ get
+ {
+ _glyphRunMetrics ??= CreateGlyphRunMetrics();
+
+ return _glyphRunMetrics.Value;
+ }
+ }
+
///
/// Gets or sets the baseline origin of the.
///
@@ -168,19 +187,6 @@ namespace Avalonia.Media
///
public bool IsLeftToRight => ((BiDiLevel & 1) == 0);
- ///
- /// Gets or sets the conservative bounding box of the .
- ///
- public Size Size
- {
- get
- {
- _size ??= CalculateSize();
-
- return _size.Value;
- }
- }
-
///
/// The platform implementation of the .
///
@@ -232,16 +238,7 @@ namespace Avalonia.Media
for (var i = 0; i < glyphIndex; i++)
{
- if (GlyphAdvances.IsEmpty)
- {
- var glyph = GlyphIndices[i];
-
- distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
- }
- else
- {
- distance += GlyphAdvances[i];
- }
+ distance += GetGlyphAdvance(i);
}
return distance;
@@ -282,42 +279,20 @@ namespace Avalonia.Media
var currentX = 0.0;
var index = 0;
- if (GlyphTypeface.IsFixedPitch)
+ for (; index < GlyphIndices.Length - Metrics.NewlineLength; index++)
{
- var glyph = GlyphIndices[index];
+ var advance = GetGlyphAdvance(index);
- var advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
-
- index = Math.Min(GlyphIndices.Length - 1,
- (int)Math.Round(distance / advance, MidpointRounding.AwayFromZero));
- }
- else
- {
- for (; index < GlyphIndices.Length; index++)
+ if (currentX + advance >= distance)
{
- double advance;
-
- if (GlyphAdvances.IsEmpty)
- {
- var glyph = GlyphIndices[index];
-
- advance = GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
- }
- else
- {
- advance = GlyphAdvances[index];
- }
-
- if (currentX + advance >= distance)
- {
- break;
- }
-
- currentX += advance;
+ break;
}
+
+ currentX += advance;
}
- var characterHit = FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
+ var characterHit =
+ FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width);
var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
@@ -343,7 +318,8 @@ namespace Avalonia.Media
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
}
- var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+ var nextCharacterHit =
+ FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
@@ -368,14 +344,6 @@ namespace Avalonia.Media
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
}
- private class ReverseComparer : IComparer
- {
- public int Compare(T x, T y)
- {
- return Comparer.Default.Compare(y, x);
- }
- }
-
///
/// Finds a glyph index for given character index.
///
@@ -472,7 +440,7 @@ namespace Avalonia.Media
if (GlyphClusters.IsEmpty)
{
- width = GetGlyphWidth(index);
+ width = GetGlyphAdvance(index);
return new CharacterHit(start, 1);
}
@@ -485,7 +453,7 @@ namespace Avalonia.Media
while (nextCluster == cluster)
{
- width += GetGlyphWidth(currentIndex);
+ width += GetGlyphAdvance(currentIndex);
if (IsLeftToRight)
{
@@ -528,16 +496,16 @@ namespace Avalonia.Media
///
/// The glyph index.
/// The glyph's width.
- private double GetGlyphWidth(int index)
+ private double GetGlyphAdvance(int index)
{
- if (GlyphAdvances.IsEmpty)
+ if (!GlyphAdvances.IsEmpty)
{
- var glyph = GlyphIndices[index];
-
- return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ return GlyphAdvances[index];
}
- return GlyphAdvances[index];
+ var glyph = GlyphIndices[index];
+
+ return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
///
@@ -549,34 +517,90 @@ namespace Avalonia.Media
return new Point(0, -GlyphTypeface.Ascent * Scale);
}
- ///
- /// Calculates the size of the .
- ///
- ///
- /// The calculated bounds.
- ///
- private Size CalculateSize()
+ private GlyphRunMetrics CreateGlyphRunMetrics()
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
- var width = 0.0;
+ var widthIncludingTrailingWhitespace = 0d;
+ var width = 0d;
+
+ var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength);
- if (GlyphAdvances.IsEmpty)
+ for (var index = 0; index < _glyphIndices.Length; index++)
{
- foreach (var glyph in GlyphIndices)
+ var advance = GetGlyphAdvance(index);
+
+ widthIncludingTrailingWhitespace += advance;
+
+ if (index > _glyphIndices.Length - 1 - trailingWhitespaceLength)
{
- width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ continue;
+ }
+
+ width += advance;
+ }
+
+ return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
+ height);
+ }
+
+ private int GetTrailingWhitespaceLength(out int newLineLength)
+ {
+ newLineLength = 0;
+
+ if (_characters.IsEmpty)
+ {
+ return 0;
+ }
+
+ var trailingWhitespaceLength = 0;
+
+ if (_glyphClusters.IsEmpty)
+ {
+ for (var i = _characters.Length - 1; i >= 0;)
+ {
+ var codepoint = Codepoint.ReadAt(_characters, i, out var count);
+
+ if (!codepoint.IsWhiteSpace)
+ {
+ break;
+ }
+
+ if (codepoint.IsBreakChar)
+ {
+ newLineLength++;
+ }
+
+ trailingWhitespaceLength++;
+
+ i -= count;
}
}
else
{
- foreach (var advance in GlyphAdvances)
+ for (var i = _glyphClusters.Length - 1; i >= 0; i--)
{
- width += advance;
+ var cluster = _glyphClusters[i];
+
+ var codepointIndex = cluster - _characters.Start;
+
+ var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
+
+ if (!codepoint.IsWhiteSpace)
+ {
+ break;
+ }
+
+ if (codepoint.IsBreakChar)
+ {
+ newLineLength++;
+ }
+
+ trailingWhitespaceLength++;
}
}
- return new Size(width, height);
+ return trailingWhitespaceLength;
}
private void Set(ref T field, T value)
@@ -586,6 +610,10 @@ namespace Avalonia.Media
throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
}
+ _glyphRunMetrics = null;
+
+ _baselineOrigin = null;
+
field = value;
}
@@ -613,16 +641,20 @@ namespace Avalonia.Media
var platformRenderInterface = AvaloniaLocator.Current.GetService();
- _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
-
- var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
-
- _size = new Size(width, height);
+ _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this);
}
void IDisposable.Dispose()
{
_glyphRunImpl?.Dispose();
}
+
+ private class ReverseComparer : IComparer
+ {
+ public int Compare(T x, T y)
+ {
+ return Comparer.Default.Compare(y, x);
+ }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs b/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs
new file mode 100644
index 0000000000..a8698a7d82
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs
@@ -0,0 +1,25 @@
+namespace Avalonia.Media
+{
+ public readonly struct GlyphRunMetrics
+ {
+ public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
+ int newlineLength, double height)
+ {
+ Width = width;
+ WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
+ TrailingWhitespaceLength = trailingWhitespaceLength;
+ NewlineLength = newlineLength;
+ Height = height;
+ }
+
+ public double Width { get; }
+
+ public double WidthIncludingTrailingWhitespace { get; }
+
+ public int TrailingWhitespaceLength { get; }
+
+ public int NewlineLength { get; }
+
+ public double Height { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
index 35aa62aa65..2be505b9c0 100644
--- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -5,6 +5,8 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
+ public const int InvisibleGlyph = 3;
+
public GlyphTypeface(Typeface typeface)
: this(FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface))
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
index 338c92f6b1..3757a4506a 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
@@ -14,6 +14,7 @@
/// Draws the at the given origin.
///
/// The drawing context.
- public abstract void Draw(DrawingContext drawingContext);
+ /// The origin.
+ public abstract void Draw(DrawingContext drawingContext, Point origin);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
index 8e7d934bca..dccad1e647 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
@@ -1,33 +1,144 @@
namespace Avalonia.Media.TextFormatting
{
- public class GenericTextParagraphProperties : TextParagraphProperties
+ ///
+ /// Generic implementation of TextParagraphProperties
+ ///
+ public sealed class GenericTextParagraphProperties : TextParagraphProperties
{
+ private FlowDirection _flowDirection;
private TextAlignment _textAlignment;
- private TextWrapping _textWrapping;
+ private TextWrapping _textWrap;
private double _lineHeight;
- public GenericTextParagraphProperties(
- TextRunProperties defaultTextRunProperties,
+ ///
+ /// Constructing TextParagraphProperties
+ ///
+ /// default paragraph's default run properties
+ /// logical horizontal alignment
+ /// text wrap option
+ /// Paragraph line height
+ public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
- TextWrapping textWrapping = TextWrapping.NoWrap,
+ TextWrapping textWrap = TextWrapping.NoWrap,
double lineHeight = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
+ _textAlignment = textAlignment;
+ _textWrap = textWrap;
+ _lineHeight = lineHeight;
+ }
+ ///
+ /// Constructing TextParagraphProperties
+ ///
+ /// text flow direction
+ /// logical horizontal alignment
+ /// true if the paragraph is the first line in the paragraph
+ /// true if the line is always collapsible
+ /// default paragraph's default run properties
+ /// text wrap option
+ /// Paragraph line height
+ /// line indentation
+ public GenericTextParagraphProperties(
+ FlowDirection flowDirection,
+ TextAlignment textAlignment,
+ bool firstLineInParagraph,
+ bool alwaysCollapsible,
+ TextRunProperties defaultTextRunProperties,
+ TextWrapping textWrap,
+ double lineHeight,
+ double indent
+ )
+ {
+ _flowDirection = flowDirection;
_textAlignment = textAlignment;
+ FirstLineInParagraph = firstLineInParagraph;
+ AlwaysCollapsible = alwaysCollapsible;
+ DefaultTextRunProperties = defaultTextRunProperties;
+ _textWrap = textWrap;
+ _lineHeight = lineHeight;
+ Indent = indent;
+ }
- _textWrapping = textWrapping;
+ ///
+ /// Constructing TextParagraphProperties from another one
+ ///
+ /// source line props
+ public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
+ : this(textParagraphProperties.FlowDirection,
+ textParagraphProperties.TextAlignment,
+ textParagraphProperties.FirstLineInParagraph,
+ textParagraphProperties.AlwaysCollapsible,
+ textParagraphProperties.DefaultTextRunProperties,
+ textParagraphProperties.TextWrapping,
+ textParagraphProperties.LineHeight,
+ textParagraphProperties.Indent)
+ {
+ }
- _lineHeight = lineHeight;
+ ///
+ /// This property specifies whether the primary text advance
+ /// direction shall be left-to-right, right-to-left, or top-to-bottom.
+ ///
+ public override FlowDirection FlowDirection
+ {
+ get { return _flowDirection; }
}
+ ///
+ /// This property describes how inline content of a block is aligned.
+ ///
+ public override TextAlignment TextAlignment
+ {
+ get { return _textAlignment; }
+ }
+
+ ///
+ /// Paragraph's line height
+ ///
+ public override double LineHeight
+ {
+ get { return _lineHeight; }
+ }
+
+ ///
+ /// Indicates the first line of the paragraph.
+ ///
+ public override bool FirstLineInParagraph { get; }
+
+ ///
+ /// If true, the formatted line may always be collapsed. If false (the default),
+ /// only lines that overflow the paragraph width are collapsed.
+ ///
+ public override bool AlwaysCollapsible { get; }
+
+ ///
+ /// Paragraph's default run properties
+ ///
public override TextRunProperties DefaultTextRunProperties { get; }
- public override TextAlignment TextAlignment => _textAlignment;
+ ///
+ /// This property controls whether or not text wraps when it reaches the flow edge
+ /// of its containing block box
+ ///
+ public override TextWrapping TextWrapping
+ {
+ get { return _textWrap; }
+ }
+
+ ///
+ /// Line indentation
+ ///
+ public override double Indent { get; }
- public override TextWrapping TextWrapping => _textWrapping;
+ ///
+ /// Set text flow direction
+ ///
+ internal void SetFlowDirection(FlowDirection flowDirection)
+ {
+ _flowDirection = flowDirection;
+ }
- public override double LineHeight => _lineHeight;
///
/// Set text alignment
@@ -37,20 +148,21 @@
_textAlignment = textAlignment;
}
+
///
- /// Set text wrap
+ /// Set line height
///
- internal void SetTextWrapping(TextWrapping textWrapping)
+ internal void SetLineHeight(double lineHeight)
{
- _textWrapping = textWrapping;
+ _lineHeight = lineHeight;
}
///
- /// Set line height
+ /// Set text wrap
///
- internal void SetLineHeight(double lineHeight)
+ internal void SetTextWrapping(TextWrapping textWrap)
{
- _lineHeight = lineHeight;
+ _textWrap = textWrap;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
index 3db3589498..7756ca5c8f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
@@ -7,8 +7,11 @@ namespace Avalonia.Media.TextFormatting
///
public class GenericTextRunProperties : TextRunProperties
{
- public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = 12,
- TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null, IBrush backgroundBrush = null,
+ private const double DefaultFontRenderingEmSize = 12;
+
+ public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = DefaultFontRenderingEmSize,
+ TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null,
+ IBrush backgroundBrush = null, BaselineAlignment baselineAlignment = BaselineAlignment.Baseline,
CultureInfo cultureInfo = null)
{
Typeface = typeface;
@@ -16,6 +19,7 @@ namespace Avalonia.Media.TextFormatting
TextDecorations = textDecorations;
ForegroundBrush = foregroundBrush;
BackgroundBrush = backgroundBrush;
+ BaselineAlignment = baselineAlignment;
CultureInfo = cultureInfo;
}
@@ -34,6 +38,9 @@ namespace Avalonia.Media.TextFormatting
///
public override IBrush BackgroundBrush { get; }
+ ///
+ public override BaselineAlignment BaselineAlignment { get; }
+
///
public override CultureInfo CultureInfo { get; }
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs b/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs
new file mode 100644
index 0000000000..5ff0d0c38f
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs
@@ -0,0 +1,14 @@
+namespace Avalonia.Media.TextFormatting
+{
+ public enum LogicalDirection
+ {
+ ///
+ /// Backward, or from right to left.
+ ///
+ Backward,
+ ///
+ /// Forward, or from left to right.
+ ///
+ Forward
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index 9f6f2b2f43..723d5e81ab 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -45,38 +45,41 @@ namespace Avalonia.Media.TextFormatting
public GlyphRun GlyphRun { get; }
///
- public override void Draw(DrawingContext drawingContext)
+ public override void Draw(DrawingContext drawingContext, Point origin)
{
- if (GlyphRun.GlyphIndices.Length == 0)
+ using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin)))
{
- return;
- }
-
- if (Properties.Typeface == default)
- {
- return;
- }
-
- if (Properties.ForegroundBrush == null)
- {
- return;
- }
-
- if (Properties.BackgroundBrush != null)
- {
- drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
- }
-
- drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);
-
- if (Properties.TextDecorations == null)
- {
- return;
- }
-
- foreach (var textDecoration in Properties.TextDecorations)
- {
- textDecoration.Draw(drawingContext, this);
+ if (GlyphRun.GlyphIndices.Length == 0)
+ {
+ return;
+ }
+
+ if (Properties.Typeface == default)
+ {
+ return;
+ }
+
+ if (Properties.ForegroundBrush == null)
+ {
+ return;
+ }
+
+ if (Properties.BackgroundBrush != null)
+ {
+ drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
+ }
+
+ drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);
+
+ if (Properties.TextDecorations == null)
+ {
+ return;
+ }
+
+ foreach (var textDecoration in Properties.TextDecorations)
+ {
+ textDecoration.Draw(drawingContext, this);
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index b91a50a27c..c6f524451b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -16,6 +16,14 @@ namespace Avalonia.Media.TextFormatting
Properties = properties;
}
+ public TextCharacters(ReadOnlySlice text, int offsetToFirstCharacter, int length,
+ TextRunProperties properties)
+ {
+ Text = text.Skip(offsetToFirstCharacter).Take(length);
+ TextSourceLength = length;
+ Properties = properties;
+ }
+
///
public override int TextSourceLength { get; }
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
index fd71fb53e7..21e354a119 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
@@ -5,5 +5,11 @@
///
public class TextEndOfLine : TextRun
{
+ public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
+ {
+ TextSourceLength = textSourceLength;
+ }
+
+ public override int TextSourceLength { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 6ae5258323..300d61f81d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -12,7 +12,8 @@ namespace Avalonia.Media.TextFormatting
{
var textWrapping = paragraphProperties.TextWrapping;
- var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
+ var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak,
+ out var nextLineBreak);
var textRange = GetTextRange(textRuns);
@@ -21,20 +22,18 @@ namespace Avalonia.Media.TextFormatting
switch (textWrapping)
{
case TextWrapping.NoWrap:
- {
- var textLineMetrics =
- TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
-
- textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
- break;
- }
+ {
+ textLine = new TextLineImpl(textRuns, textRange, paragraphWidth, paragraphProperties,
+ nextLineBreak);
+ break;
+ }
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
- {
- textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
- nextLineBreak);
- break;
- }
+ {
+ textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
+ nextLineBreak);
+ break;
+ }
default:
throw new ArgumentOutOfRangeException();
}
@@ -51,7 +50,8 @@ namespace Avalonia.Media.TextFormatting
///
/// true if characters fit into the available width; otherwise, false.
///
- internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, out int count)
+ internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth,
+ out int count)
{
var glyphRun = textCharacters.GlyphRun;
@@ -282,21 +282,24 @@ namespace Avalonia.Media.TextFormatting
switch (textRun)
{
case TextCharacters textCharacters:
- {
- var shapeableRuns = textCharacters.GetShapeableCharacters();
-
- foreach (var run in shapeableRuns)
- {
- var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
- run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
+ {
+ var shapeableRuns = textCharacters.GetShapeableCharacters();
- var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
+ foreach (var run in shapeableRuns)
+ {
+ var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
+ run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
- textRuns.Add(shapedCharacters);
- }
+ var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
- break;
+ textRuns.Add(shapedCharacters);
}
+
+ break;
+ }
+ case TextEndOfLine textEndOfLine:
+ nextLineBreak = new TextLineBreak(textEndOfLine);
+ break;
}
if (TryGetLineBreak(textRun, out var runLineBreak))
@@ -359,107 +362,137 @@ namespace Avalonia.Media.TextFormatting
{
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
- var runIndex = 0;
- var currentLength = 0;
+ var measuredLength = 0;
- while (runIndex < textRuns.Count)
+ foreach (var currentRun in textRuns)
{
- var currentRun = textRuns[runIndex];
-
if (currentWidth + currentRun.Size.Width > availableWidth)
{
- var breakFound = false;
+ if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count))
+ {
+ measuredLength += count;
+ }
- var currentBreakPosition = 0;
+ break;
+ }
- if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var measuredLength))
- {
- if (measuredLength < currentRun.Text.Length)
- {
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+ currentWidth += currentRun.Size.Width;
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
- {
- var nextBreakPosition = lineBreaker.Current.PositionWrap;
+ measuredLength += currentRun.Text.Length;
+ }
- if (nextBreakPosition == 0 || nextBreakPosition > measuredLength)
- {
- break;
- }
+ var currentLength = 0;
- breakFound = lineBreaker.Current.Required ||
- lineBreaker.Current.PositionWrap != currentRun.Text.Length;
+ var lastWrapPosition = 0;
- currentBreakPosition = nextBreakPosition;
- }
- }
- }
- else
+ var currentPosition = 0;
+
+ if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow)
+ {
+ measuredLength = 1;
+ }
+ else
+ {
+ for (var index = 0; index < textRuns.Count; index++)
+ {
+ var currentRun = textRuns[index];
+
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+
+ var breakFound = false;
+
+ while (lineBreaker.MoveNext())
{
- // Make sure we wrap at least one character.
- if (currentLength == 0)
+ if (lineBreaker.Current.Required &&
+ currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{
- measuredLength = 1;
+ breakFound = true;
+
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+
+ break;
}
- }
- if (breakFound)
- {
- measuredLength = currentBreakPosition;
- }
- else
- {
- if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
+ if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) &&
+ currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
{
- var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition));
-
- if (lineBreaker.MoveNext())
+ if (lastWrapPosition > 0)
{
- measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap;
+ currentPosition = lastWrapPosition;
}
+ else
+ {
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+ }
+
+ breakFound = true;
+
+ break;
}
- }
- currentLength += measuredLength;
+ if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength)
+ {
+ currentPosition = currentLength + lineBreaker.Current.PositionWrap;
- var splitResult = SplitTextRuns(textRuns, currentLength);
+ if (index < textRuns.Count - 1 &&
+ lineBreaker.Current.PositionWrap == currentRun.Text.Length)
+ {
+ var nextRun = textRuns[index + 1];
- var textLineMetrics = TextLineMetrics.Create(splitResult.First,
- new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
+ lineBreaker = new LineBreakEnumerator(nextRun.Text);
- var remainingCharacters = splitResult.Second;
+ if (lineBreaker.MoveNext() &&
+ lineBreaker.Current.PositionMeasure == 0)
+ {
+ currentPosition += lineBreaker.Current.PositionWrap;
+ }
+ }
- if (currentLineBreak?.RemainingCharacters != null)
- {
- if (remainingCharacters != null)
- {
- remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
- }
- else
- {
- remainingCharacters = new List(currentLineBreak.RemainingCharacters);
+ breakFound = true;
+
+ break;
}
+
+ lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
+ }
+
+ if (!breakFound)
+ {
+ currentLength += currentRun.Text.Length;
+
+ continue;
}
- var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
- new TextLineBreak(remainingCharacters) :
- null;
+ measuredLength = currentPosition;
- return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak);
+ break;
}
+ }
+
+ var splitResult = SplitTextRuns(textRuns, measuredLength);
- currentWidth += currentRun.Size.Width;
+ textRange = new TextRange(textRange.Start, measuredLength);
- currentLength += currentRun.GlyphRun.Characters.Length;
+ var remainingCharacters = splitResult.Second;
- runIndex++;
+ if (currentLineBreak?.RemainingCharacters != null)
+ {
+ if (remainingCharacters != null)
+ {
+ remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
+ }
+ else
+ {
+ remainingCharacters = new List(currentLineBreak.RemainingCharacters);
+ }
}
- return new TextLineImpl(textRuns,
- TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties),
- currentLineBreak?.RemainingCharacters != null ?
- new TextLineBreak(currentLineBreak.RemainingCharacters) :
- null);
+ var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
+ new TextLineBreak(remainingCharacters) :
+ null;
+
+ return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties,
+ lineBreak);
}
///
@@ -545,7 +578,7 @@ namespace Avalonia.Media.TextFormatting
_pos += Current.TextSourceLength;
- return !(Current is TextEndOfLine);
+ return true;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index daa8807bf6..ef427b0cd9 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -115,27 +115,165 @@ namespace Avalonia.Media.TextFormatting
/// Draws the text layout.
///
/// The drawing context.
- public void Draw(DrawingContext context)
+ /// The origin.
+ public void Draw(DrawingContext context, Point origin)
{
if (!TextLines.Any())
{
return;
}
+ var (currentX, currentY) = origin;
+
+ foreach (var textLine in TextLines)
+ {
+ textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
+
+ currentY += textLine.Height;
+ }
+ }
+
+ ///
+ /// Get the pixel location relative to the top-left of the layout box given the text position.
+ ///
+ /// The text position.
+ ///
+ public Rect HitTestTextPosition(int textPosition)
+ {
+ if (TextLines.Count == 0)
+ {
+ return new Rect();
+ }
+
+ if (textPosition < 0 || textPosition >= _text.Length)
+ {
+ var lastLine = TextLines[TextLines.Count - 1];
+
+ var lineX = lastLine.Width;
+
+ var lineY = Size.Height - lastLine.Height;
+
+ return new Rect(lineX, lineY, 0, lastLine.Height);
+ }
+
var currentY = 0.0;
foreach (var textLine in TextLines)
{
- var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width,
- _paragraphProperties.TextAlignment);
+ if (textLine.TextRange.End < textPosition)
+ {
+ currentY += textLine.Height;
+
+ continue;
+ }
+
+ var characterHit = new CharacterHit(textPosition);
+
+ var startX = textLine.GetDistanceFromCharacterHit(characterHit);
+
+ var nextCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
+
+ var endX = textLine.GetDistanceFromCharacterHit(nextCharacterHit);
+
+ return new Rect(startX, currentY, endX - startX, textLine.Height);
+ }
+
+ return new Rect();
+ }
+
+ public IEnumerable HitTestTextRange(int start, int length)
+ {
+ if (start + length <= 0)
+ {
+ return Array.Empty();
+ }
+
+ var result = new List(TextLines.Count);
+
+ var currentY = 0d;
+
+ foreach (var textLine in TextLines)
+ {
+ var currentX = textLine.Start;
+
+ if (textLine.TextRange.End < start)
+ {
+ currentY += textLine.Height;
+
+ continue;
+ }
+
+ if (start > textLine.TextRange.Start)
+ {
+ currentX += textLine.GetDistanceFromCharacterHit(new CharacterHit(start));
+ }
+
+ var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length));
+
+ result.Add(new Rect(currentX, currentY, endX - currentX, textLine.Height));
+
+ if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
+ {
+ break;
+ }
+
+ currentY += textLine.Height;
+ }
+
+ return result;
+ }
+
+ public TextHitTestResult HitTestPoint(in Point point)
+ {
+ var currentY = 0d;
+
+ var lineIndex = 0;
+ TextLine currentLine = null;
+ CharacterHit characterHit;
+
+ for (; lineIndex < TextLines.Count; lineIndex++)
+ {
+ currentLine = TextLines[lineIndex];
- using (context.PushPostTransform(Matrix.CreateTranslation(offsetX, currentY)))
+ if (currentY + currentLine.Height > point.Y)
{
- textLine.Draw(context);
+ characterHit = currentLine.GetCharacterHitFromDistance(point.X);
+
+ return GetHitTestResult(currentLine, characterHit, point);
}
- currentY += textLine.LineMetrics.Size.Height;
+ currentY += currentLine.Height;
}
+
+ if (currentLine is null)
+ {
+ return new TextHitTestResult();
+ }
+
+ characterHit = currentLine.GetNextCaretCharacterHit(new CharacterHit(currentLine.TextRange.End));
+
+ return GetHitTestResult(currentLine, characterHit, point);
+ }
+
+ private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
+ {
+ var (x, y) = point;
+
+ var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
+
+ var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
+
+ if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
+ {
+ lastTrailingIndex -= textLine.NewLineLength;
+ }
+
+ var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
+ y > Size.Height;
+
+ return new TextHitTestResult { IsInside = isInside, IsTrailing = isTrailing, TextPosition = textPosition };
}
///
@@ -155,7 +293,8 @@ namespace Avalonia.Media.TextFormatting
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
- return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight);
+ return new GenericTextParagraphProperties(FlowDirection.LeftToRight, textAlignment, true, false,
+ textRunStyle, textWrapping, lineHeight, 0);
}
///
@@ -166,12 +305,12 @@ namespace Avalonia.Media.TextFormatting
/// The current height.
private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
{
- if (width < textLine.LineMetrics.Size.Width)
+ if (width < textLine.Width)
{
- width = textLine.LineMetrics.Size.Width;
+ width = textLine.Width;
}
- height += textLine.LineMetrics.Size.Height;
+ height += textLine.Height;
}
///
@@ -190,8 +329,9 @@ namespace Avalonia.Media.TextFormatting
new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties)
};
- return new TextLineImpl(textRuns,
- TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties));
+ var textRange = new TextRange(startingIndex, 1);
+
+ return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties);
}
///
@@ -205,7 +345,7 @@ namespace Avalonia.Media.TextFormatting
TextLines = new List { textLine };
- Size = new Size(0, textLine.LineMetrics.Size.Height);
+ Size = new Size(0, textLine.Height);
}
else
{
@@ -230,7 +370,7 @@ namespace Avalonia.Media.TextFormatting
if (textLines.Count > 0)
{
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
- height + textLine.LineMetrics.Size.Height > MaxHeight)
+ height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
@@ -244,7 +384,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- var hasOverflowed = textLine.LineMetrics.HasOverflowed;
+ var hasOverflowed = textLine.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
@@ -257,7 +397,7 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine;
- if (currentPosition != _text.Length || textLine.TextLineBreak == null)
+ if (currentPosition != _text.Length || textLine.TextLineBreak?.RemainingCharacters == null)
{
continue;
}
@@ -290,6 +430,128 @@ namespace Avalonia.Media.TextFormatting
};
}
+ public int GetLineIndexFromCharacterIndex(int charIndex)
+ {
+ if (TextLines is null)
+ {
+ return -1;
+ }
+
+ if (charIndex < 0)
+ {
+ return -1;
+ }
+
+ if (charIndex > _text.Length - 1)
+ {
+ return TextLines.Count - 1;
+ }
+
+ for (var index = 0; index < TextLines.Count; index++)
+ {
+ var textLine = TextLines[index];
+
+ if (textLine.TextRange.End < charIndex)
+ {
+ continue;
+ }
+
+ if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End)
+ {
+ return index;
+ }
+ }
+
+ return TextLines.Count - 1;
+ }
+
+ public int GetCharacterIndexFromPoint(Point point, bool snapToText)
+ {
+ if (TextLines is null)
+ {
+ return -1;
+ }
+
+ var (x, y) = point;
+
+ if (!snapToText && y > Size.Height)
+ {
+ return -1;
+ }
+
+ var currentY = 0d;
+
+ foreach (var textLine in TextLines)
+ {
+ if (currentY + textLine.Height <= y)
+ {
+ currentY += textLine.Height;
+
+ continue;
+ }
+
+ if (x > textLine.WidthIncludingTrailingWhitespace)
+ {
+ if (snapToText)
+ {
+ return textLine.TextRange.End;
+ }
+
+ return -1;
+ }
+
+ var characterHit = textLine.GetCharacterHitFromDistance(x);
+
+ return characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ }
+
+ return _text.Length;
+ }
+
+ public Rect GetRectFromCharacterIndex(int characterIndex, bool trailingEdge)
+ {
+ if (TextLines is null)
+ {
+ return Rect.Empty;
+ }
+
+ var distanceY = 0d;
+
+ var currentIndex = 0;
+
+ foreach (var textLine in TextLines)
+ {
+ if (currentIndex + textLine.TextRange.Length < characterIndex)
+ {
+ distanceY += textLine.Height;
+
+ currentIndex += textLine.TextRange.Length;
+
+ continue;
+ }
+
+ var characterHit = new CharacterHit(characterIndex);
+
+ while (characterHit.FirstCharacterIndex < characterIndex)
+ {
+ characterHit = textLine.GetNextCaretCharacterHit(characterHit);
+ }
+
+ var distanceX = textLine.GetDistanceFromCharacterHit(trailingEdge ?
+ characterHit :
+ new CharacterHit(characterHit.FirstCharacterIndex));
+
+ if (characterHit.TrailingLength > 0)
+ {
+ distanceX += 1;
+ }
+
+ return new Rect(distanceX, distanceY, 0, textLine.Height);
+ }
+
+ return Rect.Empty;
+ }
+
private readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
@@ -306,16 +568,16 @@ namespace Avalonia.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
- if (textSourceIndex > _text.End)
+ if (textSourceIndex > _text.Length)
{
- return new TextEndOfLine();
+ return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
- return new TextEndOfLine();
+ return new TextEndOfParagraph();
}
var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 8a1efa0611..e343279a43 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -7,6 +7,14 @@ namespace Avalonia.Media.TextFormatting
///
public abstract class TextLine
{
+ ///
+ /// Gets the text runs that are contained within a line.
+ ///
+ ///
+ /// The contained text runs.
+ ///
+ public abstract IReadOnlyList TextRuns { get; }
+
///
/// Gets the text range that is covered by the line.
///
@@ -16,28 +24,28 @@ namespace Avalonia.Media.TextFormatting
public abstract TextRange TextRange { get; }
///
- /// Gets the text runs.
+ /// Gets the state of the line when broken by line breaking process.
///
- ///
- /// The text runs.
- ///
- public abstract IReadOnlyList TextRuns { get; }
+ ///
+ /// A value that represents the line break.
+ ///
+ public abstract TextLineBreak TextLineBreak { get; }
///
- /// Gets the line metrics.
+ /// Gets the distance from the top to the baseline of the current TextLine object.
///
- ///
- /// The line metrics.
- ///
- public abstract TextLineMetrics LineMetrics { get; }
+ ///
+ /// A that represents the baseline distance.
+ ///
+ public abstract double Baseline { get; }
///
- /// Gets the state of the line when broken by line breaking process.
+ /// Gets the distance from the top-most to bottom-most black pixel in a line.
///
///
- /// A value that represents the line break.
+ /// A value that represents the extent distance.
///
- public abstract TextLineBreak TextLineBreak { get; }
+ public abstract double Extent { get; }
///
/// Gets a value that indicates whether the line is collapsed.
@@ -47,11 +55,92 @@ namespace Avalonia.Media.TextFormatting
///
public abstract bool HasCollapsed { get; }
+ ///
+ /// Gets a value that indicates whether content of the line overflows the specified paragraph width.
+ ///
+ ///
+ /// true, it the line overflows the specified paragraph width; otherwise, false.
+ ///
+ public abstract bool HasOverflowed { get; }
+
+ ///
+ /// Gets the height of a line of text.
+ ///
+ ///
+ /// The text line height.
+ ///
+ public abstract double Height { get; }
+
+ ///
+ /// Gets the number of newline characters at the end of a line.
+ ///
+ ///
+ /// The number of newline characters.
+ ///
+ public abstract int NewLineLength { get; }
+
+ ///
+ /// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
+ ///
+ ///
+ /// The overhang after distance.
+ ///
+ public abstract double OverhangAfter { get; }
+
+ ///
+ /// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
+ ///
+ ///
+ /// The overhang leading distance.
+ ///
+ public abstract double OverhangLeading { get; }
+
+ ///
+ /// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
+ ///
+ ///
+ /// The overhang trailing distance.
+ ///
+ public abstract double OverhangTrailing { get; }
+
+ ///
+ /// Gets the distance from the start of a paragraph to the starting point of a line.
+ ///
+ ///
+ /// The distance from the start of a paragraph to the starting point of a line.
+ ///
+ public abstract double Start { get; }
+
+ ///
+ /// Gets the number of whitespace code points beyond the last non-blank character in a line.
+ ///
+ ///
+ /// The number of whitespace code points beyond the last non-blank character in a line.
+ ///
+ public abstract int TrailingWhitespaceLength { get; }
+
+ ///
+ /// Gets the width of a line of text, excluding trailing whitespace characters.
+ ///
+ ///
+ /// The text line width, excluding trailing whitespace characters.
+ ///
+ public abstract double Width { get; }
+
+ ///
+ /// Gets the width of a line of text, including trailing whitespace characters.
+ ///
+ ///
+ /// The text line width, including trailing whitespace characters.
+ ///
+ public abstract double WidthIncludingTrailingWhitespace { get; }
+
///
/// Draws the at the given origin.
///
/// The drawing context.
- public abstract void Draw(DrawingContext drawingContext);
+ ///
+ public abstract void Draw(DrawingContext drawingContext, Point lineOrigin);
///
/// Create a collapsed line based on collapsed text properties.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
index c24454cb76..4ee0a9a28f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -4,10 +4,20 @@ namespace Avalonia.Media.TextFormatting
{
public class TextLineBreak
{
+ public TextLineBreak(TextEndOfLine textEndOfLine)
+ {
+ TextEndOfLine = textEndOfLine;
+ }
+
public TextLineBreak(IReadOnlyList remainingCharacters)
{
RemainingCharacters = remainingCharacters;
}
+
+ ///
+ /// Get the
+ ///
+ public TextEndOfLine TextEndOfLine { get; }
///
/// Get the remaining shaped characters that were split up by the during the formatting process.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index fc98e9f6f8..b68e50c54e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -1,30 +1,36 @@
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
+using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- private readonly List _textRuns;
+ private readonly List _shapedTextRuns;
+ private readonly double _paragraphWidth;
+ private readonly TextParagraphProperties _paragraphProperties;
+ private readonly TextLineMetrics _textLineMetrics;
- public TextLineImpl(List textRuns, TextLineMetrics lineMetrics,
- TextLineBreak lineBreak = null, bool hasCollapsed = false)
+ public TextLineImpl(List textRuns, TextRange textRange, double paragraphWidth,
+ TextParagraphProperties paragraphProperties, TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
- _textRuns = textRuns;
- LineMetrics = lineMetrics;
+ TextRange = textRange;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
- }
- ///
- public override TextRange TextRange => LineMetrics.TextRange;
+ _shapedTextRuns = textRuns;
+ _paragraphWidth = paragraphWidth;
+ _paragraphProperties = paragraphProperties;
+
+ _textLineMetrics = CreateLineMetrics();
+ }
///
- public override IReadOnlyList TextRuns => _textRuns;
+ public override IReadOnlyList TextRuns => _shapedTextRuns;
///
- public override TextLineMetrics LineMetrics { get; }
+ public override TextRange TextRange { get; }
///
public override TextLineBreak TextLineBreak { get; }
@@ -33,19 +39,52 @@ namespace Avalonia.Media.TextFormatting
public override bool HasCollapsed { get; }
///
- public override void Draw(DrawingContext drawingContext)
+ public override bool HasOverflowed => _textLineMetrics.HasOverflowed;
+
+ ///
+ public override double Baseline => _textLineMetrics.TextBaseline;
+
+ ///
+ public override double Extent => _textLineMetrics.Height;
+
+ ///
+ public override double Height => _textLineMetrics.Height;
+
+ ///
+ public override int NewLineLength => _textLineMetrics.NewLineLength;
+
+ ///
+ public override double OverhangAfter => 0;
+
+ ///
+ public override double OverhangLeading => 0;
+
+ ///
+ public override double OverhangTrailing => 0;
+
+ ///
+ public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
+
+ ///
+ public override double Start => _textLineMetrics.Start;
+
+ ///
+ public override double Width => _textLineMetrics.Width;
+
+ ///
+ public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace;
+
+ ///
+ public override void Draw(DrawingContext drawingContext, Point lineOrigin)
{
- var currentX = 0.0;
+ var (currentX, currentY) = lineOrigin;
- foreach (var textRun in _textRuns)
+ foreach (var textRun in _shapedTextRuns)
{
- var offsetY = LineMetrics.TextBaseline - textRun.GlyphRun.BaselineOrigin.Y;
-
- using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, offsetY)))
- {
- textRun.Draw(drawingContext);
- }
+ var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
+ textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
+
currentX += textRun.Size.Width;
}
}
@@ -59,19 +98,19 @@ namespace Avalonia.Media.TextFormatting
}
var collapsingProperties = collapsingPropertiesList[0];
+
var runIndex = 0;
var currentWidth = 0.0;
var textRange = TextRange;
var collapsedLength = 0;
- TextLineMetrics textLineMetrics;
var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width;
- while (runIndex < _textRuns.Count)
+ while (runIndex < _shapedTextRuns.Count)
{
- var currentRun = _textRuns[runIndex];
+ var currentRun = _shapedTextRuns[runIndex];
currentWidth += currentRun.Size.Width;
@@ -87,14 +126,14 @@ namespace Avalonia.Media.TextFormatting
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
- var nextBreakPosition = lineBreaker.Current.PositionWrap;
+ var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
- if (nextBreakPosition > measuredLength)
+ if (nextBreakPosition >= measuredLength)
{
break;
}
@@ -108,7 +147,7 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
- var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength);
+ var splitResult = TextFormatterImpl.SplitTextRuns(_shapedTextRuns, collapsedLength);
var shapedTextCharacters = new List(splitResult.First.Count + 1);
@@ -118,12 +157,8 @@ namespace Avalonia.Media.TextFormatting
textRange = new TextRange(textRange.Start, collapsedLength);
- var shapedWidth = GetShapedWidth(shapedTextCharacters);
-
- textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
- LineMetrics.TextBaseline, textRange, false);
-
- return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
+ return new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
+ TextLineBreak, true);
}
availableWidth -= currentRun.Size.Width;
@@ -133,17 +168,71 @@ namespace Avalonia.Media.TextFormatting
runIndex++;
}
- textLineMetrics =
- new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width),
- LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed);
+ return this;
+ }
+
+ private TextLineMetrics CreateLineMetrics()
+ {
+ var width = 0d;
+ var widthIncludingWhitespace = 0d;
+ var trailingWhitespaceLength = 0;
+ var newLineLength = 0;
+ var ascent = 0d;
+ var descent = 0d;
+ var lineGap = 0d;
+
+ for (var index = 0; index < _shapedTextRuns.Count; index++)
+ {
+ var textRun = _shapedTextRuns[index];
+
+ var fontMetrics =
+ new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+
+ if (ascent > fontMetrics.Ascent)
+ {
+ ascent = fontMetrics.Ascent;
+ }
+
+ if (descent < fontMetrics.Descent)
+ {
+ descent = fontMetrics.Descent;
+ }
+
+ if (lineGap < fontMetrics.LineGap)
+ {
+ lineGap = fontMetrics.LineGap;
+ }
+
+ if (index == _shapedTextRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ }
+ else
+ {
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+ }
+ }
+
+ var start = GetParagraphOffsetX(width, _paragraphWidth, _paragraphProperties.TextAlignment);
- return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null,
- true);
+ var lineHeight = _paragraphProperties.LineHeight;
+
+ var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
+ descent - ascent + lineGap :
+ lineHeight;
+
+ return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
+ -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
}
///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
+ distance -= Start;
+
if (distance < 0)
{
// hit happens before the line, return the first position
@@ -153,7 +242,7 @@ namespace Avalonia.Media.TextFormatting
// process hit that happens within the line
var characterHit = new CharacterHit();
- foreach (var run in _textRuns)
+ foreach (var run in _shapedTextRuns)
{
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@@ -171,7 +260,32 @@ namespace Avalonia.Media.TextFormatting
///
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
- return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
+ var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
+
+ if (characterIndex > TextRange.End)
+ {
+ if (NewLineLength > 0)
+ {
+ return Start + Width;
+ }
+ return Start + WidthIncludingTrailingWhitespace;
+ }
+
+ var currentDistance = Start;
+
+ foreach (var textRun in _shapedTextRuns)
+ {
+ if (characterIndex > textRun.Text.End)
+ {
+ currentDistance += textRun.Size.Width;
+
+ continue;
+ }
+
+ return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex));
+ }
+
+ return currentDistance;
}
///
@@ -189,7 +303,7 @@ namespace Avalonia.Media.TextFormatting
var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
- var textRun = _textRuns[runIndex];
+ var textRun = _shapedTextRuns[runIndex];
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
@@ -219,28 +333,6 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- ///
- /// Get distance from line start to the specified codepoint index.
- ///
- private double DistanceFromCodepointIndex(int codepointIndex)
- {
- var currentDistance = 0.0;
-
- foreach (var textRun in _textRuns)
- {
- if (codepointIndex > textRun.Text.End)
- {
- currentDistance += textRun.Size.Width;
-
- continue;
- }
-
- return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex));
- }
-
- return currentDistance;
- }
-
///
/// Tries to find the next character hit.
///
@@ -258,26 +350,28 @@ namespace Avalonia.Media.TextFormatting
return false; // Cannot go forward anymore
}
+ if (codepointIndex < TextRange.Start)
+ {
+ codepointIndex = TextRange.Start;
+ }
+
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
- while (runIndex < TextRuns.Count)
+ while (runIndex < _shapedTextRuns.Count)
{
- var run = _textRuns[runIndex];
+ var run = _shapedTextRuns[runIndex];
- var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+ var foundCharacterHit =
+ run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length;
var characterIndex = codepointIndex - run.Text.Start;
- var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _);
-
- if (codepoint.IsBreakChar)
+ if (characterIndex < 0 && characterHit.TrailingLength == 0)
{
- foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _);
-
- isAtEnd = true;
+ foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
@@ -323,7 +417,7 @@ namespace Avalonia.Media.TextFormatting
while (runIndex >= 0)
{
- var run = _textRuns[runIndex];
+ var run = _shapedTextRuns[runIndex];
var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
@@ -349,9 +443,9 @@ namespace Avalonia.Media.TextFormatting
/// The text run index.
private int GetRunIndexAtCodepointIndex(int codepointIndex)
{
- if (codepointIndex >= TextRange.End)
+ if (codepointIndex > TextRange.End)
{
- return _textRuns.Count - 1;
+ return _shapedTextRuns.Count - 1;
}
if (codepointIndex <= 0)
@@ -361,11 +455,11 @@ namespace Avalonia.Media.TextFormatting
var runIndex = 0;
- while (runIndex < _textRuns.Count)
+ while (runIndex < _shapedTextRuns.Count)
{
- var run = _textRuns[runIndex];
+ var run = _shapedTextRuns[runIndex];
- if (run.Text.End > codepointIndex)
+ if (run.Text.End >= codepointIndex)
{
return runIndex;
}
@@ -392,24 +486,5 @@ namespace Avalonia.Media.TextFormatting
return new ShapedTextCharacters(glyphRun, textRun.Properties);
}
-
- ///
- /// Gets the shaped width of specified shaped text characters.
- ///
- /// The shaped text characters.
- ///
- /// The shaped width.
- ///
- private static double GetShapedWidth(IReadOnlyList shapedTextCharacters)
- {
- var shapedWidth = 0.0;
-
- for (var i = 0; i < shapedTextCharacters.Count; i++)
- {
- shapedWidth += shapedTextCharacters[i].Size.Width;
- }
-
- return shapedWidth;
- }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
index c4d7527659..1799c9d3db 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using Avalonia.Utilities;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
///
/// Represents a metric for a objects,
@@ -9,29 +6,39 @@ namespace Avalonia.Media.TextFormatting
///
public readonly struct TextLineMetrics
{
- public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed)
+ public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
+ int trailingWhitespaceLength, double width,
+ double widthIncludingTrailingWhitespace)
{
- Size = size;
- TextBaseline = textBaseline;
- TextRange = textRange;
HasOverflowed = hasOverflowed;
+ Height = height;
+ NewLineLength = newLineLength;
+ Start = start;
+ TextBaseline = textBaseline;
+ TrailingWhitespaceLength = trailingWhitespaceLength;
+ Width = width;
+ WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
}
-
+
///
- /// Gets the text range that is covered by the text line.
+ /// Gets a value that indicates whether content of the line overflows the specified paragraph width.
///
- ///
- /// The text range that is covered by the text line.
- ///
- public TextRange TextRange { get; }
+ public bool HasOverflowed { get; }
///
- /// Gets the size of the text line.
+ /// Gets the height of a line of text.
///
- ///
- /// The size.
- ///
- public Size Size { get; }
+ public double Height { get; }
+
+ ///
+ /// Gets the number of newline characters at the end of a line.
+ ///
+ public int NewLineLength { get; }
+
+ ///
+ /// Gets the distance from the start of a paragraph to the starting point of a line.
+ ///
+ public double Start { get; }
///
/// Gets the distance from the top to the baseline of the line of text.
@@ -39,58 +46,18 @@ namespace Avalonia.Media.TextFormatting
public double TextBaseline { get; }
///
- /// Gets a boolean value that indicates whether content of the line overflows
- /// the specified paragraph width.
+ /// Gets the number of whitespace code points beyond the last non-blank character in a line.
///
- public bool HasOverflowed { get; }
+ public int TrailingWhitespaceLength { get; }
///
- /// Creates the text line metrics.
+ /// Gets the width of a line of text, excluding trailing whitespace characters.
///
- /// The text runs.
- /// The text range that is covered by the text line.
- /// The paragraph width.
- /// The text alignment.
- ///
- public static TextLineMetrics Create(IEnumerable textRuns, TextRange textRange, double paragraphWidth,
- TextParagraphProperties paragraphProperties)
- {
- var lineWidth = 0.0;
- var ascent = 0.0;
- var descent = 0.0;
- var lineGap = 0.0;
+ public double Width { get; }
- foreach (var textRun in textRuns)
- {
- var shapedRun = (ShapedTextCharacters)textRun;
-
- var fontMetrics =
- new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize);
-
- lineWidth += shapedRun.Size.Width;
-
- if (ascent > fontMetrics.Ascent)
- {
- ascent = fontMetrics.Ascent;
- }
-
- if (descent < fontMetrics.Descent)
- {
- descent = fontMetrics.Descent;
- }
-
- if (lineGap < fontMetrics.LineGap)
- {
- lineGap = fontMetrics.LineGap;
- }
- }
-
- var size = new Size(lineWidth,
- double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ?
- descent - ascent + lineGap :
- paragraphProperties.LineHeight);
-
- return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth);
- }
+ ///
+ /// Gets the width of a line of text, including trailing whitespace characters.
+ ///
+ public double WidthIncludingTrailingWhitespace { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index 3ecd1aafd9..4eff3fbd83 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -5,11 +5,36 @@
///
public abstract class TextParagraphProperties
{
+ ///
+ /// This property specifies whether the primary text advance
+ /// direction shall be left-to-right, right-to-left, or top-to-bottom.
+ ///
+ public abstract FlowDirection FlowDirection { get; }
+
///
/// Gets the text alignment.
///
public abstract TextAlignment TextAlignment { get; }
+ ///
+ /// Paragraph's line height
+ ///
+ public abstract double LineHeight { get; }
+
+ ///
+ /// Indicates the first line of the paragraph.
+ ///
+ public abstract bool FirstLineInParagraph { get; }
+
+ ///
+ /// If true, the formatted line may always be collapsed. If false (the default),
+ /// only lines that overflow the paragraph width are collapsed.
+ ///
+ public virtual bool AlwaysCollapsible
+ {
+ get { return false; }
+ }
+
///
/// Gets the default text style.
///
@@ -27,8 +52,16 @@
public abstract TextWrapping TextWrapping { get; }
///
- /// Paragraph's line height
+ /// Line indentation
///
- public abstract double LineHeight { get; }
+ public abstract double Indent { get; }
+
+ ///
+ /// Paragraph indentation
+ ///
+ public virtual double ParagraphIndent
+ {
+ get { return 0; }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
index c15a771755..42cb5a7c46 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
[DebuggerTypeProxy(typeof(TextRunDebuggerProxy))]
public abstract class TextRun
{
- public static readonly int DefaultTextSourceLength = 1;
+ public const int DefaultTextSourceLength = 1;
///
/// Gets the text source length.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
index c4f9443c3d..468df33ab1 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -42,6 +42,11 @@ namespace Avalonia.Media.TextFormatting
///
public abstract CultureInfo CultureInfo { get; }
+ ///
+ /// Run vertical box alignment
+ ///
+ public abstract BaselineAlignment BaselineAlignment { get; }
+
public bool Equals(TextRunProperties other)
{
if (ReferenceEquals(null, other))
@@ -66,7 +71,7 @@ namespace Avalonia.Media.TextFormatting
{
unchecked
{
- var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0);
+ var hashCode = Typeface.GetHashCode();
hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode();
hashCode = (hashCode * 397) ^ (TextDecorations != null ? TextDecorations.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (ForegroundBrush != null ? ForegroundBrush.GetHashCode() : 0);
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index fb4a4427b7..f0dbfee718 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -136,9 +136,8 @@ namespace Avalonia.Platform
/// Creates a platform implementation of a glyph run.
///
/// The glyph run.
- /// The glyph run's width.
///
- IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
+ IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun);
bool SupportsIndividualRoundRects { get; }
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 6d0be9f64d..6b7f0e11cf 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -171,7 +171,7 @@ namespace Avalonia.Skia
private static readonly ThreadLocal s_textBlobBuilderThreadLocal = new ThreadLocal(() => new SKTextBlobBuilder());
///
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
var count = glyphRun.GlyphIndices.Length;
var textBlobBuilder = s_textBlobBuilderThreadLocal.Value;
@@ -183,11 +183,8 @@ namespace Avalonia.Skia
s_font.Size = (float)glyphRun.FontRenderingEmSize;
s_font.Typeface = typeface;
-
SKTextBlob textBlob;
- width = 0;
-
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
@@ -197,8 +194,6 @@ namespace Avalonia.Skia
textBlobBuilder.AddRun(glyphRun.GlyphIndices.Buffer.Span, s_font);
textBlob = textBlobBuilder.Build();
-
- width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
@@ -206,6 +201,8 @@ namespace Avalonia.Skia
var positions = buffer.GetPositionSpan();
+ var width = 0d;
+
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
@@ -251,13 +248,10 @@ namespace Avalonia.Skia
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
- width = currentX;
-
textBlob = textBlobBuilder.Build();
}
return new GlyphRunImpl(textBlob);
-
}
public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi)
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 6ae27870e8..15685c0187 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -213,7 +213,7 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
@@ -236,8 +236,6 @@ namespace Avalonia.Direct2D1
run.Advances = new float[glyphCount];
- width = 0;
-
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphAdvances.IsEmpty)
@@ -247,8 +245,6 @@ namespace Avalonia.Direct2D1
var advance = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
run.Advances[i] = advance;
-
- width += advance;
}
}
else
@@ -258,8 +254,6 @@ namespace Avalonia.Direct2D1
var advance = (float)glyphRun.GlyphAdvances[i];
run.Advances[i] = advance;
-
- width += advance;
}
}
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 1570205456..268977d662 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -86,10 +86,8 @@ namespace Avalonia.Benchmarks
return new MockFontManagerImpl();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- width = default;
-
return new NullGlyphRun();
}
diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
index 42ec392066..304b8c5cfd 100644
--- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
@@ -14,10 +14,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
public class VisualBrushTests : TestBase
{
- //Whitespaces are used here to be able to compare rendering results in a platform independent way.
- //Otherwise tests will fail because of slightly different glyph rendering.
- private static readonly string s_visualBrushText = " ";
-
public VisualBrushTests()
: base(@"Media\VisualBrush")
{
@@ -46,13 +42,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
BorderThickness = new Thickness(2),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
- Child = new TextBlock
+ Child = new Panel
{
- FontSize = 24,
- FontFamily = TestFontFamily,
- Background = Brushes.Green,
- Foreground = Brushes.Yellow,
- Text = s_visualBrushText
+ Height = 26,
+ Width = 150,
+ Background = Brushes.Green
}
}
}
@@ -392,10 +386,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
Background = Brushes.Yellow,
HorizontalAlignment = HorizontalAlignment.Left,
- Child = new TextBlock
+ Child = new Panel
{
- FontFamily = TestFontFamily,
- Text = s_visualBrushText
+ Height = 10,
+ Width = 50
}
}),
new Border
diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
new file mode 100644
index 0000000000..f9c45e7d22
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests.Media
+{
+ public class GlyphRunTests
+ {
+ [InlineData("ABC \r", 29, 4, 1)]
+ [InlineData("ABC \r", 23, 3, 1)]
+ [InlineData("ABC \r", 17, 2, 1)]
+ [InlineData("ABC \r", 11, 1, 1)]
+ [InlineData("ABC \r", 7, 1, 0)]
+ [InlineData("ABC \r", 5, 0, 1)]
+ [InlineData("ABC \r", 2, 0, 0)]
+ [Theory]
+ public void Should_Get_Distance_From_CharacterHit(string text, double distance, int expectedIndex,
+ int expectedTrailingLength)
+ {
+ using (Start())
+ {
+ var glyphRun =
+ TextShaper.Current.ShapeText(text.AsMemory(), Typeface.Default, 10, CultureInfo.CurrentCulture);
+
+ var characterHit = glyphRun.GetCharacterHitFromDistance(distance, out _);
+
+ Assert.Equal(expectedIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(expectedTrailingLength, characterHit.TrailingLength);
+ }
+ }
+
+ private static IDisposable Start()
+ {
+ var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
+ .With(renderInterface: new PlatformRenderInterface(null),
+ textShaperImpl: new TextShaperImpl(),
+ fontManagerImpl: new CustomFontManagerImpl()));
+
+ return disposable;
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
deleted file mode 100644
index 6a5065939e..0000000000
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
-
-namespace Avalonia.Skia.UnitTests.Media.TextFormatting
-{
- internal class FormattableTextSource : ITextSource
- {
- private readonly ReadOnlySlice _text;
- private readonly TextRunProperties _defaultStyle;
- private ReadOnlySlice> _styleSpans;
-
- public FormattableTextSource(string text, TextRunProperties defaultStyle,
- ReadOnlySlice> styleSpans)
- {
- _text = text.AsMemory();
-
- _defaultStyle = defaultStyle;
-
- _styleSpans = styleSpans;
- }
-
- public TextRun GetTextRun(int textSourceIndex)
- {
- if (_styleSpans.IsEmpty)
- {
- return new TextEndOfParagraph();
- }
-
- var currentSpan = _styleSpans[0];
-
- _styleSpans = _styleSpans.Skip(1);
-
- return new TextCharacters(_text.AsSlice(currentSpan.Start, currentSpan.Length),
- _defaultStyle);
- }
- }
-}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
new file mode 100644
index 0000000000..e3a2f6e766
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+
+namespace Avalonia.Skia.UnitTests.Media.TextFormatting
+{
+ internal readonly struct FormattedTextSource : ITextSource
+ {
+ private readonly ReadOnlySlice _text;
+ private readonly TextRunProperties _defaultProperties;
+ private readonly IReadOnlyList> _textModifier;
+
+ public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties,
+ IReadOnlyList> textModifier)
+ {
+ _text = text;
+ _defaultProperties = defaultProperties;
+ _textModifier = textModifier;
+ }
+
+ public TextRun GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex > _text.End)
+ {
+ return null;
+ }
+
+ var runText = _text.Skip(textSourceIndex);
+
+ if (runText.IsEmpty)
+ {
+ return new TextEndOfParagraph();
+ }
+
+ var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
+
+ return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
+ }
+
+ ///
+ /// Creates a span of text run properties that has modifier applied.
+ ///
+ /// The text to create the properties for.
+ /// The default text properties.
+ /// The text properties modifier.
+ ///
+ /// The created text style run.
+ ///
+ private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
+ TextRunProperties defaultProperties, IReadOnlyList> textModifier)
+ {
+ if (textModifier == null || textModifier.Count == 0)
+ {
+ return new ValueSpan(text.Start, text.Length, defaultProperties);
+ }
+
+ var currentProperties = defaultProperties;
+
+ var hasOverride = false;
+
+ var i = 0;
+
+ var length = 0;
+
+ for (; i < textModifier.Count; i++)
+ {
+ var propertiesOverride = textModifier[i];
+
+ var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
+
+ if (textRange.End < text.Start)
+ {
+ continue;
+ }
+
+ if (textRange.Start > text.End)
+ {
+ length = text.Length;
+ break;
+ }
+
+ if (textRange.Start > text.Start)
+ {
+ if (propertiesOverride.Value != currentProperties)
+ {
+ length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
+
+ break;
+ }
+ }
+
+ length += Math.Min(text.Length - length, textRange.Length);
+
+ if (hasOverride)
+ {
+ continue;
+ }
+
+ hasOverride = true;
+
+ currentProperties = propertiesOverride.Value;
+ }
+
+ if (length < text.Length && i == textModifier.Count)
+ {
+ if (currentProperties == defaultProperties)
+ {
+ length = text.Length;
+ }
+ }
+
+ if (length != text.Length)
+ {
+ text = text.Take(length);
+ }
+
+ return new ValueSpan(text.Start, length, currentProperties);
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
index 40aa862906..2a20fdd9fe 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
@@ -20,6 +20,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
+ if (textSourceIndex > 50)
+ {
+ return null;
+ }
+
if (textSourceIndex == 50)
{
return new TextEndOfParagraph();
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
index 045deacd0b..65c342b065 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
@@ -17,8 +17,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun GetTextRun(int textSourceIndex)
{
+ if (textSourceIndex > _text.Length)
+ {
+ return null;
+ }
+
var runText = _text.Skip(textSourceIndex);
-
+
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 7f9713930a..05dd32b84d 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -81,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan(9, 1, defaultProperties)
};
- var textSource = new FormattableTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
+ var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns);
var formatter = new TextFormatterImpl();
@@ -167,7 +167,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, 1,
- new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow));
+ new GenericTextParagraphProperties(defaultProperties, textWrap : TextWrapping.WrapWithOverflow));
if (text.Length - currentPosition > expectedCharactersPerLine)
{
@@ -223,7 +223,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, paragraphWidth,
- new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap));
+ new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
Assert.True(expected.Contains(textLine.TextRange.End));
@@ -256,7 +256,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties, lineHeight: 50));
- Assert.Equal(50, textLine.LineMetrics.Size.Height);
+ Assert.Equal(50, textLine.Height);
}
}
@@ -273,7 +273,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
@@ -286,7 +286,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
- Assert.True(textLine.LineMetrics.Size.Width <= 200);
+ Assert.True(textLine.Width <= 200);
textSourceIndex += textLine.TextRange.Length;
}
@@ -301,7 +301,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
const string text = "012345";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
- var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
@@ -321,6 +321,87 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
+ new []{ "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod "})]
+
+ [Theory]
+ public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
+ {
+ using (Start())
+ {
+ var typeface = new Typeface("Verdana");
+
+ var defaultProperties = new GenericTextRunProperties(typeface, 32, foregroundBrush: Brushes.Black);
+
+ var styleSpans = new[]
+ {
+ new ValueSpan(0, 5,
+ new GenericTextRunProperties(typeface, 48)),
+ new ValueSpan(6, 11,
+ new GenericTextRunProperties(new Typeface("Verdana", weight: FontWeight.Bold), 32)),
+ new ValueSpan(28, 28,
+ new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
+ };
+
+ var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
+
+ var formatter = new TextFormatterImpl();
+
+ var currentPosition = 0;
+
+ var currentHeight = 0d;
+
+ var currentLineIndex = 0;
+
+ while (currentPosition < text.Length && currentLineIndex < expectedLines.Length)
+ {
+ var textLine =
+ formatter.FormatLine(textSource, currentPosition, 300,
+ new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
+
+ currentPosition += textLine.TextRange.Length;
+
+ if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
+ {
+ textLine = textLine.Collapse(new TextTrailingWordEllipsis(300, defaultProperties));
+ }
+
+ currentHeight += textLine.Height;
+
+ var currentText = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+
+ Assert.Equal(expectedLines[currentLineIndex], currentText);
+
+ currentLineIndex++;
+ }
+
+ Assert.Equal(expectedLines.Length,currentLineIndex);
+ }
+ }
+
+ [InlineData(TextAlignment.Left)]
+ [InlineData(TextAlignment.Center)]
+ [InlineData(TextAlignment.Right)]
+ [Theory]
+ public void Should_Align_TextLine(TextAlignment textAlignment)
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textAlignment);
+
+ var textSource = new SingleBufferTextSource("0123456789", defaultProperties);
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, 100, paragraphProperties);
+
+ var expectedOffset = TextLine.GetParagraphOffsetX(textLine.Width, 100, textAlignment);
+
+ Assert.Equal(expectedOffset, textLine.Start);
+ }
+ }
+
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index f7bc75c05d..afa1fbf461 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -11,8 +11,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLayoutTests
{
- private static readonly string s_singleLineText = "0123456789";
- private static readonly string s_multiLineText = "012345678\r\r0123456789";
+ private const string SingleLineText = "0123456789";
+ private const string MultiLineText = "01 23 45 678\r\rabc def gh ij";
[InlineData("01234\r01234\r", 3)]
[InlineData("01234\r01234", 2)]
@@ -45,7 +45,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -61,7 +61,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var actual = textRun.Text.Buffer.Span.ToString();
- Assert.Equal("12", actual);
+ Assert.Equal("1 ", actual);
Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
}
@@ -74,7 +74,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
- for (var i = 4; i < s_multiLineText.Length; i++)
+ var expected = new TextLayout(
+ MultiLineText,
+ Typeface.Default,
+ 12.0f,
+ Brushes.Black.ToImmutable(),
+ textWrapping: TextWrapping.Wrap,
+ maxWidth: 25);
+
+ var expectedLines = expected.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
+ x.TextRange.Length)).ToList();
+
+ for (var i = 4; i < MultiLineText.Length; i++)
{
var spans = new[]
{
@@ -82,16 +93,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
- var expected = new TextLayout(
- s_multiLineText,
- Typeface.Default,
- 12.0f,
- Brushes.Black.ToImmutable(),
- textWrapping: TextWrapping.Wrap,
- maxWidth: 25);
-
var actual = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -99,14 +102,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 25,
textStyleOverrides: spans);
- Assert.Equal(expected.TextLines.Count, actual.TextLines.Count);
+ var actualLines = actual.TextLines.Select(x => MultiLineText.Substring(x.TextRange.Start,
+ x.TextRange.Length)).ToList();
+
+ Assert.Equal(expectedLines.Count, actualLines.Count);
for (var j = 0; j < actual.TextLines.Count; j++)
{
- Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length);
+ var expectedText = expectedLines[j];
+
+ var actualText = actualLines[j];
- Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length),
- actual.TextLines[j].TextRuns.Sum(x => x.Text.Length));
+ Assert.Equal(expectedText, actualText);
}
}
}
@@ -126,7 +133,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_singleLineText,
+ SingleLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -140,7 +147,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Text.Length);
- var actual = s_singleLineText.Substring(textRun.Text.Start,
+ var actual = SingleLineText.Substring(textRun.Text.Start,
textRun.Text.Length);
Assert.Equal("01", actual);
@@ -163,7 +170,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_singleLineText,
+ SingleLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -261,12 +268,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable());
- Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
+ Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
}
}
@@ -276,13 +283,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable());
Assert.Equal(
- s_multiLineText.Length,
+ MultiLineText.Length,
layout.TextLines.Select(textLine =>
textLine.TextRuns.Sum(textRun => textRun.Text.Length))
.Sum());
@@ -295,9 +302,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
const string text =
- "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
- "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. " +
- "Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.";
+ "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet";
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
@@ -338,7 +343,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
};
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
@@ -541,7 +546,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_multiLineText,
+ MultiLineText,
Typeface.Default,
12,
Brushes.Black,
@@ -549,7 +554,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var line in layout.TextLines)
{
- Assert.Equal(50, line.LineMetrics.Size.Height);
+ Assert.Equal(50, line.Height);
}
}
}
@@ -601,7 +606,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var layout = new TextLayout(
- s_singleLineText,
+ SingleLineText,
Typeface.Default,
12,
Brushes.Black,
@@ -609,7 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
maxWidth: 3);
//every character should be new line as there not enough space for even one character
- Assert.Equal(s_singleLineText.Length, layout.TextLines.Count);
+ Assert.Equal(SingleLineText.Length, layout.TextLines.Count);
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 8f1bd5979a..5961806c5c 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -71,6 +71,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
+ [InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
[Theory]
public void Should_Get_Next_Caret_CharacterHit(string text)
@@ -109,9 +110,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
- for (var i = 0; i < clusters.Length; i++)
+ foreach (var cluster in clusters)
{
- Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex);
+ Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex);
nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
}
@@ -127,6 +128,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
+ [InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
[Theory]
public void Should_Get_Previous_Caret_CharacterHit(string text)
@@ -269,14 +271,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width);
+ characterHit = textLine.GetCharacterHitFromDistance(textLine.Width);
Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex);
}
}
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
- [InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234 \u2026")]
+ [InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234\u2026")]
[Theory]
public void Should_Collapse_Line(string text, int numberOfCharacters, TextCollapsingStyle style, string expected)
{
@@ -333,8 +335,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- [Fact]
- public void Should_Ignore_Invisible_Characters()
+ [Fact(Skip = "Verify this")]
+ public void Should_Ignore_NewLine_Characters()
{
using (Start())
{
@@ -356,6 +358,28 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(new CharacterHit(8, 2), nextCharacterHit);
}
}
+
+ [Fact]
+ public void TextLineBreak_Should_Contain_TextEndOfLine()
+ {
+ using (Start())
+ {
+ var defaultTextRunProperties =
+ new GenericTextRunProperties(Typeface.Default);
+
+ const string text = "0123456789";
+
+ var source = new SingleBufferTextSource(text, defaultTextRunProperties);
+
+ var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
+
+ var formatter = TextFormatter.Current;
+
+ var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
+
+ Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ }
+ }
private static IDisposable Start()
{
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index e73a76357a..8015172e72 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -97,9 +97,8 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- width = 0;
return Mock.Of();
}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 6d0683e699..1a6d003062 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -52,7 +52,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
throw new NotImplementedException();
}
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
index ea81296017..110b44a4c0 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
index a8ec09df99..a74af7a06e 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
index 485b03acd3..7e4faf77d5 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
index a6bc0daee3..3b91d04f7c 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
index 8c622c37b5..1df122151d 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
index bfe5fd7fbb..aedc6afe30 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
index f3c3c4aee2..ccdcab84e4 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
index 80116d81c6..2d7ccac2f7 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
index f366912df5..d9c62a72a8 100644
Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
index 474e2bb2bf..97ca065be8 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
index 4ca25db112..e3e6297c96 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
index 3c9c88e8f1..c0565f8963 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
index e9efe1e796..cdf11c52d6 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
index b864ffca33..ae2f2a4c21 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
index fa359eac11..c8cdded708 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
index 778e3f6c13..3cde267980 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
index d7ef920967..6468f46b51 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ
diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
index 152c703f93..901b44a0ed 100644
Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png differ