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