Browse Source

Merge branch 'master' into master

pull/5826/head
Collin Alpert 5 years ago
committed by GitHub
parent
commit
4df9b64bbc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/PULL_REQUEST_TEMPLATE.md
  2. 76
      src/Avalonia.Controls/Primitives/AccessText.cs
  3. 25
      src/Avalonia.Controls/TextBlock.cs
  4. 3
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  5. 2
      src/Avalonia.Input/MouseDevice.cs
  6. 59
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  7. 32
      src/Avalonia.Visuals/Media/BaselineAlignment.cs
  8. 25
      src/Avalonia.Visuals/Media/FlowDirection.cs
  9. 212
      src/Avalonia.Visuals/Media/GlyphRun.cs
  10. 25
      src/Avalonia.Visuals/Media/GlyphRunMetrics.cs
  11. 2
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  12. 3
      src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
  13. 144
      src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
  14. 11
      src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs
  15. 14
      src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs
  16. 63
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  17. 8
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  18. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs
  19. 221
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  20. 300
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  21. 117
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  22. 10
      src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
  23. 265
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  24. 101
      src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
  25. 37
      src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
  26. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
  27. 7
      src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
  28. 3
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  29. 12
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  30. 8
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  31. 4
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  32. 20
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  33. 46
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  34. 38
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs
  35. 121
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs
  36. 5
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
  37. 7
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
  38. 95
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  39. 71
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  40. 36
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  41. 3
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  42. 2
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  43. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  44. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  45. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
  46. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
  47. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
  48. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
  49. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
  50. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  51. BIN
      tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png
  52. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png
  53. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  54. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png
  55. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png
  56. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png
  57. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png
  58. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png
  59. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png
  60. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

2
.github/PULL_REQUEST_TEMPLATE.md

@ -23,6 +23,8 @@
## Breaking changes
<!--- List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes -->
## Obsoletions / Deprecations
<!--- Obsolete and Deprecated attributes on APIs MUST only be included when discussed with Core team. @grokys, @kekekeks & @danwalmsley -->
## Fixed issues
<!--- If the pull request fixes issue(s) list them like this:

76
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
}
}
/// <summary>
/// Get the pixel location relative to the top-left of the layout box given the text position.
/// </summary>
/// <param name="textPosition">The text position.</param>
/// <returns></returns>
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();
}
/// <inheritdoc/>
protected override TextLayout CreateTextLayout(Size constraint, string text)
{

25
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));
}
/// <summary>

3
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();
}

2
src/Avalonia.Input/MouseDevice.cs

@ -435,7 +435,7 @@ namespace Avalonia.Input
IInputElement? branch = null;
var el = element;
IInputElement? el = element;
while (el != null)
{

59
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.TextRun>, 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

32
src/Avalonia.Visuals/Media/BaselineAlignment.cs

@ -0,0 +1,32 @@
namespace Avalonia.Media
{
/// <summary>
/// Enum specifying where a box should be positioned Vertically
/// </summary>
public enum BaselineAlignment
{
/// <summary>Align top toward top of container</summary>
Top,
/// <summary>Center vertically</summary>
Center,
/// <summary>Align bottom toward bottom of container</summary>
Bottom,
/// <summary>Align at baseline</summary>
Baseline,
/// <summary>Align toward text's top of container</summary>
TextTop,
/// <summary>Align toward text's bottom of container</summary>
TextBottom,
/// <summary>Align baseline to subscript position of container</summary>
Subscript,
/// <summary>Align baseline to superscript position of container</summary>
Superscript,
}
}

25
src/Avalonia.Visuals/Media/FlowDirection.cs

@ -0,0 +1,25 @@
namespace Avalonia.Media
{
/// <summary>
/// The 'flow-direction' property specifies whether the primary text advance
/// direction shall be left-to-right or right-to-left.
/// </summary>
public enum FlowDirection
{
/// <internalonly>
/// 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'.
/// </internalonly>
LeftToRight,
/// <internalonly>
/// 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'.
/// </internalonly>
RightToLeft
}
}

212
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<ushort> _glyphIndices;
private ReadOnlySlice<double> _glyphAdvances;
@ -90,6 +91,24 @@ namespace Avalonia.Media
set => Set(ref _fontRenderingEmSize, value);
}
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);
/// <summary>
///
/// </summary>
public GlyphRunMetrics Metrics
{
get
{
_glyphRunMetrics ??= CreateGlyphRunMetrics();
return _glyphRunMetrics.Value;
}
}
/// <summary>
/// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary>
@ -168,19 +187,6 @@ namespace Avalonia.Media
/// </summary>
public bool IsLeftToRight => ((BiDiLevel & 1) == 0);
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size
{
get
{
_size ??= CalculateSize();
return _size.Value;
}
}
/// <summary>
/// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary>
@ -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<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
@ -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
/// </summary>
/// <param name="index">The glyph index.</param>
/// <returns>The glyph's width.</returns>
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;
}
/// <summary>
@ -549,34 +517,90 @@ namespace Avalonia.Media
return new Point(0, -GlyphTypeface.Ascent * Scale);
}
/// <summary>
/// Calculates the size of the <see cref="GlyphRun"/>.
/// </summary>
/// <returns>
/// The calculated bounds.
/// </returns>
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<T>(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<IPlatformRenderInterface>();
_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<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
}
}

25
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; }
}
}

2
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))
{

3
src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs

@ -14,6 +14,7 @@
/// Draws the <see cref="DrawableTextRun"/> at the given origin.
/// </summary>
/// <param name="drawingContext">The drawing context.</param>
public abstract void Draw(DrawingContext drawingContext);
/// <param name="origin">The origin.</param>
public abstract void Draw(DrawingContext drawingContext, Point origin);
}
}

144
src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs

@ -1,33 +1,144 @@
namespace Avalonia.Media.TextFormatting
{
public class GenericTextParagraphProperties : TextParagraphProperties
/// <summary>
/// Generic implementation of TextParagraphProperties
/// </summary>
public sealed class GenericTextParagraphProperties : TextParagraphProperties
{
private FlowDirection _flowDirection;
private TextAlignment _textAlignment;
private TextWrapping _textWrapping;
private TextWrapping _textWrap;
private double _lineHeight;
public GenericTextParagraphProperties(
TextRunProperties defaultTextRunProperties,
/// <summary>
/// Constructing TextParagraphProperties
/// </summary>
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
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;
}
/// <summary>
/// Constructing TextParagraphProperties
/// </summary>
/// <param name="flowDirection">text flow direction</param>
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="firstLineInParagraph">true if the paragraph is the first line in the paragraph</param>
/// <param name="alwaysCollapsible">true if the line is always collapsible</param>
/// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param>
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;
/// <summary>
/// Constructing TextParagraphProperties from another one
/// </summary>
/// <param name="textParagraphProperties">source line props</param>
public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
: this(textParagraphProperties.FlowDirection,
textParagraphProperties.TextAlignment,
textParagraphProperties.FirstLineInParagraph,
textParagraphProperties.AlwaysCollapsible,
textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight,
textParagraphProperties.Indent)
{
}
_lineHeight = lineHeight;
/// <summary>
/// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left, or top-to-bottom.
/// </summary>
public override FlowDirection FlowDirection
{
get { return _flowDirection; }
}
/// <summary>
/// This property describes how inline content of a block is aligned.
/// </summary>
public override TextAlignment TextAlignment
{
get { return _textAlignment; }
}
/// <summary>
/// Paragraph's line height
/// </summary>
public override double LineHeight
{
get { return _lineHeight; }
}
/// <summary>
/// Indicates the first line of the paragraph.
/// </summary>
public override bool FirstLineInParagraph { get; }
/// <summary>
/// If true, the formatted line may always be collapsed. If false (the default),
/// only lines that overflow the paragraph width are collapsed.
/// </summary>
public override bool AlwaysCollapsible { get; }
/// <summary>
/// Paragraph's default run properties
/// </summary>
public override TextRunProperties DefaultTextRunProperties { get; }
public override TextAlignment TextAlignment => _textAlignment;
/// <summary>
/// This property controls whether or not text wraps when it reaches the flow edge
/// of its containing block box
/// </summary>
public override TextWrapping TextWrapping
{
get { return _textWrap; }
}
/// <summary>
/// Line indentation
/// </summary>
public override double Indent { get; }
public override TextWrapping TextWrapping => _textWrapping;
/// <summary>
/// Set text flow direction
/// </summary>
internal void SetFlowDirection(FlowDirection flowDirection)
{
_flowDirection = flowDirection;
}
public override double LineHeight => _lineHeight;
/// <summary>
/// Set text alignment
@ -37,20 +148,21 @@
_textAlignment = textAlignment;
}
/// <summary>
/// Set text wrap
/// Set line height
/// </summary>
internal void SetTextWrapping(TextWrapping textWrapping)
internal void SetLineHeight(double lineHeight)
{
_textWrapping = textWrapping;
_lineHeight = lineHeight;
}
/// <summary>
/// Set line height
/// Set text wrap
/// </summary>
internal void SetLineHeight(double lineHeight)
internal void SetTextWrapping(TextWrapping textWrap)
{
_lineHeight = lineHeight;
_textWrap = textWrap;
}
}
}

11
src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs

@ -7,8 +7,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
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
/// <inheritdoc />
public override IBrush BackgroundBrush { get; }
/// <inheritdoc />
public override BaselineAlignment BaselineAlignment { get; }
/// <inheritdoc />
public override CultureInfo CultureInfo { get; }
}

14
src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs

@ -0,0 +1,14 @@
namespace Avalonia.Media.TextFormatting
{
public enum LogicalDirection
{
/// <summary>
/// Backward, or from right to left.
/// </summary>
Backward,
/// <summary>
/// Forward, or from left to right.
/// </summary>
Forward
}
}

63
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -45,38 +45,41 @@ namespace Avalonia.Media.TextFormatting
public GlyphRun GlyphRun { get; }
/// <inheritdoc/>
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);
}
}
}

8
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -16,6 +16,14 @@ namespace Avalonia.Media.TextFormatting
Properties = properties;
}
public TextCharacters(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
TextRunProperties properties)
{
Text = text.Skip(offsetToFirstCharacter).Take(length);
TextSourceLength = length;
Properties = properties;
}
/// <inheritdoc />
public override int TextSourceLength { get; }

6
src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs

@ -5,5 +5,11 @@
/// </summary>
public class TextEndOfLine : TextRun
{
public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
{
TextSourceLength = textSourceLength;
}
public override int TextSourceLength { get; }
}
}

221
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
/// <returns>
/// <c>true</c> if characters fit into the available width; otherwise, <c>false</c>.
/// </returns>
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<ShapedTextCharacters>(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<ShapedTextCharacters>(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);
}
/// <summary>
@ -545,7 +578,7 @@ namespace Avalonia.Media.TextFormatting
_pos += Current.TextSourceLength;
return !(Current is TextEndOfLine);
return true;
}
}
}

300
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -115,27 +115,165 @@ namespace Avalonia.Media.TextFormatting
/// Draws the text layout.
/// </summary>
/// <param name="context">The drawing context.</param>
public void Draw(DrawingContext context)
/// <param name="origin">The origin.</param>
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;
}
}
/// <summary>
/// Get the pixel location relative to the top-left of the layout box given the text position.
/// </summary>
/// <param name="textPosition">The text position.</param>
/// <returns></returns>
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<Rect> HitTestTextRange(int start, int length)
{
if (start + length <= 0)
{
return Array.Empty<Rect>();
}
var result = new List<Rect>(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 };
}
/// <summary>
@ -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);
}
/// <summary>
@ -166,12 +305,12 @@ namespace Avalonia.Media.TextFormatting
/// <param name="height">The current height.</param>
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;
}
/// <summary>
@ -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);
}
/// <summary>
@ -205,7 +345,7 @@ namespace Avalonia.Media.TextFormatting
TextLines = new List<TextLine> { 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<char> _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);

117
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@ -7,6 +7,14 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public abstract class TextLine
{
/// <summary>
/// Gets the text runs that are contained within a line.
/// </summary>
/// <value>
/// The contained text runs.
/// </value>
public abstract IReadOnlyList<TextRun> TextRuns { get; }
/// <summary>
/// Gets the text range that is covered by the line.
/// </summary>
@ -16,28 +24,28 @@ namespace Avalonia.Media.TextFormatting
public abstract TextRange TextRange { get; }
/// <summary>
/// Gets the text runs.
/// Gets the state of the line when broken by line breaking process.
/// </summary>
/// <value>
/// The text runs.
/// </value>
public abstract IReadOnlyList<TextRun> TextRuns { get; }
/// <returns>
/// A <see cref="TextLineBreak"/> value that represents the line break.
/// </returns>
public abstract TextLineBreak TextLineBreak { get; }
/// <summary>
/// Gets the line metrics.
/// Gets the distance from the top to the baseline of the current TextLine object.
/// </summary>
/// <value>
/// The line metrics.
/// </value>
public abstract TextLineMetrics LineMetrics { get; }
/// <returns>
/// A <see cref="double"/> that represents the baseline distance.
/// </returns>
public abstract double Baseline { get; }
/// <summary>
/// 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.
/// </summary>
/// <returns>
/// A <see cref="TextLineBreak"/> value that represents the line break.
/// A value that represents the extent distance.
/// </returns>
public abstract TextLineBreak TextLineBreak { get; }
public abstract double Extent { get; }
/// <summary>
/// Gets a value that indicates whether the line is collapsed.
@ -47,11 +55,92 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
public abstract bool HasCollapsed { get; }
/// <summary>
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
/// <returns>
/// <c>true</c>, it the line overflows the specified paragraph width; otherwise, <c>false</c>.
/// </returns>
public abstract bool HasOverflowed { get; }
/// <summary>
/// Gets the height of a line of text.
/// </summary>
/// <returns>
/// The text line height.
/// </returns>
public abstract double Height { get; }
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
/// <returns>
/// The number of newline characters.
/// </returns>
public abstract int NewLineLength { get; }
/// <summary>
/// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
/// </summary>
/// <returns>
/// The overhang after distance.
/// </returns>
public abstract double OverhangAfter { get; }
/// <summary>
/// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
/// </summary>
/// <returns>
/// The overhang leading distance.
/// </returns>
public abstract double OverhangLeading { get; }
/// <summary>
/// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
/// </summary>
/// <returns>
/// The overhang trailing distance.
/// </returns>
public abstract double OverhangTrailing { get; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary>
/// <returns>
/// The distance from the start of a paragraph to the starting point of a line.
/// </returns>
public abstract double Start { get; }
/// <summary>
/// Gets the number of whitespace code points beyond the last non-blank character in a line.
/// </summary>
/// <returns>
/// The number of whitespace code points beyond the last non-blank character in a line.
/// </returns>
public abstract int TrailingWhitespaceLength { get; }
/// <summary>
/// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary>
/// <returns>
/// The text line width, excluding trailing whitespace characters.
/// </returns>
public abstract double Width { get; }
/// <summary>
/// Gets the width of a line of text, including trailing whitespace characters.
/// </summary>
/// <returns>
/// The text line width, including trailing whitespace characters.
/// </returns>
public abstract double WidthIncludingTrailingWhitespace { get; }
/// <summary>
/// Draws the <see cref="TextLine"/> at the given origin.
/// </summary>
/// <param name="drawingContext">The drawing context.</param>
public abstract void Draw(DrawingContext drawingContext);
/// <param name="lineOrigin"></param>
public abstract void Draw(DrawingContext drawingContext, Point lineOrigin);
/// <summary>
/// Create a collapsed line based on collapsed text properties.

10
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<ShapedTextCharacters> remainingCharacters)
{
RemainingCharacters = remainingCharacters;
}
/// <summary>
/// Get the
/// </summary>
public TextEndOfLine TextEndOfLine { get; }
/// <summary>
/// Get the remaining shaped characters that were split up by the <see cref="TextFormatter"/> during the formatting process.

265
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<ShapedTextCharacters> _textRuns;
private readonly List<ShapedTextCharacters> _shapedTextRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private readonly TextLineMetrics _textLineMetrics;
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
TextLineBreak lineBreak = null, bool hasCollapsed = false)
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
_textRuns = textRuns;
LineMetrics = lineMetrics;
TextRange = textRange;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
}
/// <inheritdoc/>
public override TextRange TextRange => LineMetrics.TextRange;
_shapedTextRuns = textRuns;
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
_textLineMetrics = CreateLineMetrics();
}
/// <inheritdoc/>
public override IReadOnlyList<TextRun> TextRuns => _textRuns;
public override IReadOnlyList<TextRun> TextRuns => _shapedTextRuns;
/// <inheritdoc/>
public override TextLineMetrics LineMetrics { get; }
public override TextRange TextRange { get; }
/// <inheritdoc/>
public override TextLineBreak TextLineBreak { get; }
@ -33,19 +39,52 @@ namespace Avalonia.Media.TextFormatting
public override bool HasCollapsed { get; }
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext)
public override bool HasOverflowed => _textLineMetrics.HasOverflowed;
/// <inheritdoc/>
public override double Baseline => _textLineMetrics.TextBaseline;
/// <inheritdoc/>
public override double Extent => _textLineMetrics.Height;
/// <inheritdoc/>
public override double Height => _textLineMetrics.Height;
/// <inheritdoc/>
public override int NewLineLength => _textLineMetrics.NewLineLength;
/// <inheritdoc/>
public override double OverhangAfter => 0;
/// <inheritdoc/>
public override double OverhangLeading => 0;
/// <inheritdoc/>
public override double OverhangTrailing => 0;
/// <inheritdoc/>
public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
/// <inheritdoc/>
public override double Start => _textLineMetrics.Start;
/// <inheritdoc/>
public override double Width => _textLineMetrics.Width;
/// <inheritdoc/>
public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace;
/// <inheritdoc/>
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<ShapedTextCharacters>(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<ShapedTextCharacters>(_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);
}
/// <inheritdoc/>
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
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
@ -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);
}
/// <summary>
/// Get distance from line start to the specified codepoint index.
/// </summary>
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;
}
/// <summary>
/// Tries to find the next character hit.
/// </summary>
@ -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
/// <returns>The text run index.</returns>
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);
}
/// <summary>
/// Gets the shaped width of specified shaped text characters.
/// </summary>
/// <param name="shapedTextCharacters">The shaped text characters.</param>
/// <returns>
/// The shaped width.
/// </returns>
private static double GetShapedWidth(IReadOnlyList<ShapedTextCharacters> shapedTextCharacters)
{
var shapedWidth = 0.0;
for (var i = 0; i < shapedTextCharacters.Count; i++)
{
shapedWidth += shapedTextCharacters[i].Size.Width;
}
return shapedWidth;
}
}
}

101
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
{
/// <summary>
/// Represents a metric for a <see cref="TextLine"/> objects,
@ -9,29 +6,39 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <value>
/// The text range that is covered by the text line.
/// </value>
public TextRange TextRange { get; }
public bool HasOverflowed { get; }
/// <summary>
/// Gets the size of the text line.
/// Gets the height of a line of text.
/// </summary>
/// <value>
/// The size.
/// </value>
public Size Size { get; }
public double Height { get; }
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewLineLength { get; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary>
public double Start { get; }
/// <summary>
/// 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; }
/// <summary>
/// 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.
/// </summary>
public bool HasOverflowed { get; }
public int TrailingWhitespaceLength { get; }
/// <summary>
/// Creates the text line metrics.
/// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary>
/// <param name="textRuns">The text runs.</param>
/// <param name="textRange">The text range that is covered by the text line.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text alignment.</param>
/// <returns></returns>
public static TextLineMetrics Create(IEnumerable<TextRun> 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);
}
/// <summary>
/// Gets the width of a line of text, including trailing whitespace characters.
/// </summary>
public double WidthIncludingTrailingWhitespace { get; }
}
}

37
src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs

@ -5,11 +5,36 @@
/// </summary>
public abstract class TextParagraphProperties
{
/// <summary>
/// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left, or top-to-bottom.
/// </summary>
public abstract FlowDirection FlowDirection { get; }
/// <summary>
/// Gets the text alignment.
/// </summary>
public abstract TextAlignment TextAlignment { get; }
/// <summary>
/// Paragraph's line height
/// </summary>
public abstract double LineHeight { get; }
/// <summary>
/// Indicates the first line of the paragraph.
/// </summary>
public abstract bool FirstLineInParagraph { get; }
/// <summary>
/// If true, the formatted line may always be collapsed. If false (the default),
/// only lines that overflow the paragraph width are collapsed.
/// </summary>
public virtual bool AlwaysCollapsible
{
get { return false; }
}
/// <summary>
/// Gets the default text style.
/// </summary>
@ -27,8 +52,16 @@
public abstract TextWrapping TextWrapping { get; }
/// <summary>
/// Paragraph's line height
/// Line indentation
/// </summary>
public abstract double LineHeight { get; }
public abstract double Indent { get; }
/// <summary>
/// Paragraph indentation
/// </summary>
public virtual double ParagraphIndent
{
get { return 0; }
}
}
}

2
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;
/// <summary>
/// Gets the text source length.

7
src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs

@ -42,6 +42,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public abstract CultureInfo CultureInfo { get; }
/// <summary>
/// Run vertical box alignment
/// </summary>
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);

3
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -136,9 +136,8 @@ namespace Avalonia.Platform
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun);
bool SupportsIndividualRoundRects { get; }

12
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -171,7 +171,7 @@ namespace Avalonia.Skia
private static readonly ThreadLocal<SKTextBlobBuilder> s_textBlobBuilderThreadLocal = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder());
/// <inheritdoc />
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)

8
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;
}
}

4
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();
}

20
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

46
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;
}
}
}

38
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs

@ -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<char> _text;
private readonly TextRunProperties _defaultStyle;
private ReadOnlySlice<ValueSpan<TextRunProperties>> _styleSpans;
public FormattableTextSource(string text, TextRunProperties defaultStyle,
ReadOnlySlice<ValueSpan<TextRunProperties>> 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);
}
}
}

121
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<char> _text;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>> 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);
}
/// <summary>
/// Creates a span of text run properties that has modifier applied.
/// </summary>
/// <param name="text">The text to create the properties for.</param>
/// <param name="defaultProperties">The default text properties.</param>
/// <param name="textModifier">The text properties modifier.</param>
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>> textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
return new ValueSpan<TextRunProperties>(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<TextRunProperties>(text.Start, length, currentProperties);
}
}
}

5
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();

7
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();

95
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -81,7 +81,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan<TextRunProperties>(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<TextRunProperties>(0, 5,
new GenericTextRunProperties(typeface, 48)),
new ValueSpan<TextRunProperties>(6, 11,
new GenericTextRunProperties(new Typeface("Verdana", weight: FontWeight.Bold), 32)),
new ValueSpan<TextRunProperties>(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

71
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);
}
}

36
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()
{

3
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<IGlyphRunImpl>();
}

2
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();
}

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 403 B

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Uniform_NoTile.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Loading…
Cancel
Save