Browse Source

Fix TextLineImpl.GetCharacterHitFromDistance for lines that include a TextEndOfLine run

Fix overhang handling to prevent clipped text for slanted glyphs
Fix TextBox selection issues
pull/11294/head
Benedikt Stebner 3 years ago
parent
commit
f153c626f8
  1. 7
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  3. 210
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  4. 61
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  5. 52
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  6. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  7. 8
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  8. 4
      src/Avalonia.Controls/SelectableTextBlock.cs
  9. 8
      src/Avalonia.Controls/TextBlock.cs
  10. 119
      src/Avalonia.Controls/TextBox.cs
  11. 9
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  12. 36
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  13. 44
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  14. 11
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  15. 13
      src/Skia/Avalonia.Skia/SKFontCache.cs
  16. 4
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  17. 9
      src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
  18. 2
      tests/Avalonia.RenderTests/Controls/TextBlockTests.cs
  19. 12
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  20. 6
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  21. 22
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  22. BIN
      tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
  23. BIN
      tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

7
src/Avalonia.Base/Media/GlyphRun.cs

@ -155,6 +155,8 @@ namespace Avalonia.Media
/// </summary>
public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height));
public Rect InkBounds => PlatformImpl.Item.Bounds;
/// <summary>
///
/// </summary>
@ -728,7 +730,7 @@ namespace Avalonia.Media
clusterLength++;
i--;
if(characterIndex >= 0)
if (characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
@ -827,8 +829,7 @@ namespace Avalonia.Media
GlyphTypeface,
FontRenderingEmSize,
GlyphInfos,
BaselineOrigin,
Bounds);
BaselineOrigin);
_platformImpl = RefCountable.Create(platformImpl);

2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting
if (Properties.BackgroundBrush != null)
{
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, new Rect(Size));
drawingContext.DrawRectangle(Properties.BackgroundBrush, null, GlyphRun.Bounds);
}
drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun);

210
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -13,6 +13,7 @@ namespace Avalonia.Media.TextFormatting
private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming;
private readonly TextLine[] _textLines;
private readonly CachedMetrics _metrics = new();
private int _textSourceLength;
@ -151,12 +152,95 @@ namespace Avalonia.Media.TextFormatting
=> _textLines;
/// <summary>
/// Gets the bounds of the layout.
/// The distance from the top of the first line to the bottom of the last line.
/// </summary>
/// <value>
/// The bounds.
/// </value>
public Rect Bounds { get; private set; }
public double Height
{
get
{
return _metrics.Height;
}
}
/// <summary>
/// The distance from the topmost black pixel of the first line
/// to the bottommost black pixel of the last line.
/// </summary>
public double Extent
{
get
{
return _metrics.Extent;
}
}
/// <summary>
/// The distance from the top of the first line to the baseline of the first line.
/// </summary>
public double Baseline
{
get
{
return _metrics.Baseline;
}
}
/// <summary>
/// The distance from the bottom of the last line to the extent bottom.
/// </summary>
public double OverhangAfter
{
get
{
return _metrics.OverhangAfter;
}
}
/// <summary>
/// The maximum distance from the leading black pixel to the leading alignment point of a line.
/// </summary>
public double OverhangLeading
{
get
{
return _metrics.OverhangLeading;
}
}
/// <summary>
/// The maximum distance from the trailing black pixel to the trailing alignment point of a line.
/// </summary>
public double OverhangTrailing
{
get
{
return _metrics.OverhangTrailing;
}
}
/// <summary>
/// The maximum advance width between the leading and trailing alignment points of a line,
/// excluding the width of whitespace characters at the end of the line.
/// </summary>
public double Width
{
get
{
return _metrics.Width;
}
}
/// <summary>
/// The maximum advance width between the leading and trailing alignment points of a line,
/// including the width of whitespace characters at the end of the line.
/// </summary>
public double WidthIncludingTrailingWhitespace
{
get
{
return _metrics.WidthIncludingTrailingWhitespace;
}
}
/// <summary>
/// Draws the text layout.
@ -382,7 +466,7 @@ namespace Avalonia.Media.TextFormatting
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom;
y > Height;
if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{
@ -422,41 +506,25 @@ namespace Avalonia.Media.TextFormatting
textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
}
/// <summary>
/// Updates the current bounds.
/// </summary>
/// <param name="textLine">The text line.</param>
/// <param name="left">The current left.</param>
/// <param name="width">The current width.</param>
/// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
private TextLine[] CreateTextLines()
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
if (width < lineWidth)
{
width = lineWidth;
}
var objectPool = FormattingObjectPool.Instance;
var start = textLine.Start;
var lineStartOfLongestLine = double.MaxValue;
var origin = new Point();
var first = true;
if (left > start)
{
left = start;
}
height += textLine.Height;
}
double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
private TextLine[] CreateTextLines()
{
var objectPool = FormattingObjectPool.Instance;
accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
accBlackBoxRight = accBlackBoxBottom = double.MinValue;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0, 0, 0, textLine.Height);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
return new TextLine[] { textLine };
}
@ -465,8 +533,6 @@ namespace Avalonia.Media.TextFormatting
try
{
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
_textSourceLength = 0;
TextLine? previousLine = null;
@ -487,7 +553,8 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height);
UpdateMetrics(emptyTextLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
}
break;
@ -497,7 +564,7 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
&& height + textLine.Height > MaxHeight)
&& Height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
@ -519,7 +586,8 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
previousLine = textLine;
@ -528,7 +596,7 @@ namespace Avalonia.Media.TextFormatting
{
if (textLine.TextLineBreak is { IsSplit: true })
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(WidthIncludingTrailingWhitespace));
}
break;
@ -546,18 +614,17 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first,
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
}
Bounds = new Rect(left, 0, width, height);
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var justificationWidth = MaxWidth;
if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{
justificationWidth = width;
justificationWidth = WidthIncludingTrailingWhitespace;
}
if (justificationWidth > 0)
@ -582,6 +649,46 @@ namespace Avalonia.Media.TextFormatting
}
}
private void UpdateMetrics(
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
ref double accBlackBoxLeft,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxBottom)
{
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading;
var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing;
var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
var blackBoxTop = blackBoxBottom - currentLine.Extent;
accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft);
accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight);
accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom);
accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop);
_metrics.OverhangAfter = currentLine.OverhangAfter;
_metrics.Height += currentLine.Height;
_metrics.Width = Math.Max(_metrics.Width, currentLine.Width);
_metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace);
lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start);
_metrics.Extent = accBlackBoxBottom - accBlackBoxTop;
_metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine;
_metrics.OverhangTrailing = _metrics.Width - (accBlackBoxRight - lineStartOfLongestLine);
if (first)
{
_metrics.Baseline = currentLine.Baseline;
first = false;
}
origin = origin.WithY(origin.Y + currentLine.Height);
}
/// <summary>
/// Gets the <see cref="TextCollapsingProperties"/> for current text trimming mode.
/// </summary>
@ -605,5 +712,24 @@ namespace Avalonia.Media.TextFormatting
line.Dispose();
}
}
private class CachedMetrics
{
// vertical
public double Height;
public double Baseline;
// horizontal
public double Width;
public double WidthIncludingTrailingWhitespace;
// vertical bounding box metrics
public double Extent;
public double OverhangAfter;
// horizontal bounding box metrics
public double OverhangLeading;
public double OverhangTrailing;
}
}
}

61
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public override double Baseline => _textLineMetrics.TextBaseline;
/// <inheritdoc/>
public override double Extent => _textLineMetrics.Height;
public override double Extent => _textLineMetrics.Extent;
/// <inheritdoc/>
public override double Height => _textLineMetrics.Height;
@ -60,13 +60,13 @@ namespace Avalonia.Media.TextFormatting
public override int NewLineLength => _textLineMetrics.NewlineLength;
/// <inheritdoc/>
public override double OverhangAfter => 0;
public override double OverhangAfter => _textLineMetrics.OverhangAfter;
/// <inheritdoc/>
public override double OverhangLeading => 0;
public override double OverhangLeading => _textLineMetrics.OverhangLeading;
/// <inheritdoc/>
public override double OverhangTrailing => 0;
public override double OverhangTrailing => _textLineMetrics.OverhangTrailing;
/// <inheritdoc/>
public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
@ -87,13 +87,18 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
if (textRun is DrawableTextRun drawable)
switch (textRun)
{
var offsetY = GetBaselineOffset(this, drawable);
case DrawableTextRun drawableTextRun:
{
var offsetY = GetBaselineOffset(this, drawableTextRun);
drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY));
currentX += drawableTextRun.Size.Width;
currentX += drawable.Size.Width;
break;
}
}
}
}
@ -174,10 +179,12 @@ namespace Avalonia.Media.TextFormatting
distance -= Start;
var lastIndex = _textRuns.Length - 1;
var lineLength = Length;
if (_textRuns[lastIndex] is TextEndOfLine)
if (_textRuns[lastIndex] is TextEndOfLine textEndOfLine)
{
lastIndex--;
lineLength -= textEndOfLine.Length;
}
var currentPosition = FirstTextSourceIndex;
@ -205,7 +212,7 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
currentPosition = Length - lastRun.Length;
currentPosition = lineLength - lastRun.Length;
}
return GetRunCharacterHit(lastRun, currentPosition, distance);
@ -703,7 +710,7 @@ namespace Avalonia.Media.TextFormatting
//In case a run only contains a linebreak we don't want to skip it.
if (currentRun is ShapedTextRun shaped)
{
if(currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
if (currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0)
{
continue;
}
@ -1431,9 +1438,10 @@ namespace Avalonia.Media.TextFormatting
var lineGap = fontMetrics.LineGap * scale;
var height = descent - ascent + lineGap;
var lineHeight = _paragraphProperties.LineHeight;
var bounds = new Rect();
for (var index = 0; index < _textRuns.Length; index++)
{
switch (_textRuns[index])
@ -1441,6 +1449,9 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextRun textRun:
{
var textMetrics = textRun.TextMetrics;
var glyphRun = textRun.GlyphRun;
bounds = bounds.Union(glyphRun.InkBounds);
if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
{
@ -1486,18 +1497,22 @@ namespace Avalonia.Media.TextFormatting
ascent = -drawableTextRun.Baseline;
}
bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size));
break;
}
}
}
var overhangAfter = Math.Max(0, bounds.Bottom - height);
var width = widthIncludingWhitespace;
for (var i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextRun shapedText)
if (currentRun is ShapedTextRun shapedText)
{
var glyphRun = shapedText.GlyphRun;
var glyphRunMetrics = glyphRun.Metrics;
@ -1518,6 +1533,9 @@ namespace Avalonia.Media.TextFormatting
}
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
var overhangLeading = Math.Max(0, bounds.Left - start);
var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace);
var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
@ -1527,8 +1545,21 @@ namespace Avalonia.Media.TextFormatting
}
}
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
return new TextLineMetrics
{
HasOverflowed = hasOverflowed,
Height = height,
Extent = bounds.Height,
NewlineLength = newLineLength,
Start = start,
TextBaseline = -ascent,
TrailingWhitespaceLength = trailingWhitespaceLength,
Width = width,
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
OverhangLeading= overhangLeading,
OverhangTrailing= overhangTrailing,
OverhangAfter = overhangAfter
};
}
/// <summary>

52
src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs

@ -5,59 +5,65 @@
/// that holds information about ascent, descent, line gap, size and origin of the text line.
/// </summary>
public readonly record struct TextLineMetrics
{
public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace)
{
HasOverflowed = hasOverflowed;
Height = height;
NewlineLength = newlineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
}
{
/// <summary>
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
public bool HasOverflowed { get; }
public bool HasOverflowed { get; init; }
/// <summary>
/// Gets the height of a line of text.
/// </summary>
public double Height { get; }
public double Height { get; init; }
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewlineLength { get; }
public int NewlineLength { get; init; }
/// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line.
/// </summary>
public double Start { get; }
public double Start { get; init; }
/// <summary>
/// Gets the distance from the top to the baseline of the line of text.
/// </summary>
public double TextBaseline { get; }
public double TextBaseline { get; init; }
/// <summary>
/// Gets the number of whitespace code points beyond the last non-blank character in a line.
/// </summary>
public int TrailingWhitespaceLength { get; }
public int TrailingWhitespaceLength { get; init; }
/// <summary>
/// Gets the width of a line of text, excluding trailing whitespace characters.
/// </summary>
public double Width { get; }
public double Width { get; init; }
/// <summary>
/// Gets the width of a line of text, including trailing whitespace characters.
/// </summary>
public double WidthIncludingTrailingWhitespace { get; }
public double WidthIncludingTrailingWhitespace { get; init; }
/// <summary>
/// Gets the distance from the top-most to bottom-most black pixel in a line.
/// </summary>
public double Extent { get; init; }
/// <summary>
/// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
/// </summary>
public double OverhangAfter { get; init; }
/// <summary>
/// Gets the distance that black pixels extend prior to the left leading alignment edge of the line.
/// </summary>
public double OverhangLeading { get; init; }
/// <summary>
/// Gets the distance that black pixels extend following the right trailing alignment edge of the line.
/// </summary>
public double OverhangTrailing { get; init; }
}
}

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

@ -169,9 +169,8 @@ namespace Avalonia.Platform
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="glyphInfos">The list of glyphs.</param>
/// <param name="baselineOrigin">The baseline origin of the run. Can be null.</param>
/// <param name="bounds">the conservative bounding box of the run</param>
/// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds);
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin);
/// <summary>
/// Creates a backend-specific object using a low-level API graphics context

8
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -341,7 +341,7 @@ namespace Avalonia.Controls.Presenters
var top = 0d;
var left = 0.0;
var textHeight = TextLayout.Bounds.Height;
var textHeight = TextLayout.Height;
if (Bounds.Height < textHeight)
{
@ -571,14 +571,14 @@ namespace Avalonia.Controls.Presenters
InvalidateArrange();
var measuredSize = TextLayout.Bounds.Size;
var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
return measuredSize;
return new Size(textWidth, TextLayout.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
var textWidth = Math.Ceiling(TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing);
if (finalSize.Width < textWidth)
{

4
src/Avalonia.Controls/SelectableTextBlock.cs

@ -323,8 +323,8 @@ namespace Avalonia.Controls
var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Height, 0)));
MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.WidthIncludingTrailingWhitespace, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Height, 0)));
var hit = TextLayout.HitTestPoint(point);
var textPosition = hit.TextPosition;

8
src/Avalonia.Controls/TextBlock.cs

@ -567,7 +567,7 @@ namespace Avalonia.Controls
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
var top = padding.Top;
var textHeight = TextLayout.Bounds.Height;
var textHeight = TextLayout.Height;
if (Bounds.Height < textHeight)
{
@ -588,7 +588,7 @@ namespace Avalonia.Controls
protected virtual void RenderTextLayout(DrawingContext context, Point origin)
{
TextLayout.Draw(context, origin);
TextLayout.Draw(context, origin + new Point(TextLayout.OverhangLeading, 0));
}
private bool _clearTextInternal;
@ -702,7 +702,9 @@ namespace Avalonia.Controls
}
}
return TextLayout.Bounds.Size.Inflate(padding);
var width = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
return new Size(width, TextLayout.Height).Inflate(padding);
}
protected override Size ArrangeOverride(Size finalSize)

119
src/Avalonia.Controls/TextBox.cs

@ -19,6 +19,8 @@ using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
using Avalonia.Threading;
using Avalonia.Platform;
using System.Reflection;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -100,7 +102,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="SelectionStart"/> property
/// </summary>
public static readonly StyledProperty<int> SelectionStartProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart),
coerce: CoerceCaretIndex);
/// <summary>
@ -475,7 +477,7 @@ namespace Avalonia.Controls
get => GetValue(SelectionEndProperty);
set => SetValue(SelectionEndProperty, value);
}
private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateCommandStates();
@ -486,7 +488,7 @@ namespace Avalonia.Controls
SetCurrentValue(CaretIndexProperty, value);
}
}
/// <summary>
/// Gets or sets the maximum character length of the TextBox
/// </summary>
@ -536,7 +538,7 @@ namespace Avalonia.Controls
private static string? CoerceText(AvaloniaObject sender, string? value)
{
var textBox = (TextBox)sender;
// Before #9490, snapshot here was done AFTER text change - this doesn't make sense
// since intial state would never be no text and you'd always have to make a text
// change before undo would be available
@ -988,7 +990,7 @@ namespace Avalonia.Controls
{
var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength));
textBuilder.Append(currentText);
var caretIndex = CaretIndex;
if (selectionLength != 0)
@ -1050,7 +1052,7 @@ namespace Avalonia.Controls
if (clipboard == null)
return;
await clipboard.SetTextAsync(text);
DeleteSelection();
}
@ -1417,7 +1419,7 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (_presenter == null )
if (_presenter == null)
{
return;
}
@ -1434,11 +1436,10 @@ namespace Avalonia.Controls
_presenter.MoveCaretToPoint(point);
var index = _presenter.CaretIndex;
var caretIndex = _presenter.CaretIndex;
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
SetCurrentValue(CaretIndexProperty, index);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
switch (e.ClickCount)
{
@ -1447,49 +1448,44 @@ namespace Avalonia.Controls
{
if (_wordSelectionStart >= 0)
{
var previousWord = StringUtils.PreviousWord(text, index);
if (index > _wordSelectionStart)
{
SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
}
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
{
SetCurrentValue(SelectionStartProperty, previousWord);
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
SetCurrentValue(SelectionEndProperty, caretIndex);
}
}
else
{
if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
{
SetCurrentValue(SelectionStartProperty, index);
SetCurrentValue(SelectionEndProperty, index);
_wordSelectionStart = -1;
}
SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
_wordSelectionStart = -1;
}
break;
case 2:
case 2:
if (!StringUtils.IsStartOfWord(text, index))
if (!StringUtils.IsStartOfWord(text, caretIndex))
{
SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
selectionStart = StringUtils.PreviousWord(text, caretIndex);
}
_wordSelectionStart = SelectionStart;
if (!StringUtils.IsEndOfWord(text, caretIndex))
{
selectionEnd = StringUtils.NextWord(text, caretIndex);
}
if (!StringUtils.IsEndOfWord(text, index))
if (selectionStart != selectionEnd)
{
SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
_wordSelectionStart = selectionStart;
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
break;
case 3:
_wordSelectionStart = -1;
@ -1519,30 +1515,19 @@ namespace Avalonia.Controls
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
_presenter.MoveCaretToPoint(point);
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var text = Text;
if (text != null && _wordSelectionStart >= 0)
if (_wordSelectionStart >= 0)
{
var distance = caretIndex - _wordSelectionStart;
if (distance <= 0)
{
SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex));
}
UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
if (distance >= 0)
{
if(SelectionStart != _wordSelectionStart)
{
SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
}
SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex));
}
SetCurrentValue(SelectionStartProperty, selectionStart);
SetCurrentValue(SelectionEndProperty, selectionEnd);
}
else
{
@ -1551,6 +1536,32 @@ namespace Avalonia.Controls
}
}
private void UpdateWordSelectionRange(int caretIndex, ref int selectionStart, ref int selectionEnd)
{
var text = Text;
if (string.IsNullOrEmpty(text))
{
return;
}
if (caretIndex > _wordSelectionStart)
{
var nextWord = StringUtils.NextWord(text, caretIndex);
selectionEnd = nextWord;
selectionStart = _wordSelectionStart;
}
else
{
var previousWord = StringUtils.PreviousWord(text, caretIndex);
selectionStart = previousWord;
selectionEnd = StringUtils.NextWord(text, _wordSelectionStart);
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (_presenter == null)

9
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -126,10 +126,9 @@ namespace Avalonia.Headless
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos,
Point baselineOrigin,
Rect bounds)
Point baselineOrigin)
{
return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin);
}
internal class HeadlessGlyphRunStub : IGlyphRunImpl
@ -137,13 +136,11 @@ namespace Avalonia.Headless
public HeadlessGlyphRunStub(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
Point baselineOrigin,
Rect bounds)
Point baselineOrigin)
{
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds =bounds;
}
public Rect Bounds { get; }

36
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@ -16,7 +17,7 @@ namespace Avalonia.Skia
private readonly Dictionary<SKFontEdging, SKTextBlob> _textBlobCache = new(1);
public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
if (glyphTypeface == null)
{
@ -48,9 +49,30 @@ namespace Avalonia.Skia
currentX += glyphInfos[i].GlyphAdvance;
}
_glyphTypefaceImpl.SKFont.Size = (float)fontRenderingEmSize;
var runBounds = new Rect();
var glyphBounds = ArrayPool<SKRect>.Shared.Rent(glyphInfos.Count);
_glyphTypefaceImpl.SKFont.GetGlyphWidths(_glyphIndices, null, glyphBounds);
currentX = 0;
for (var i = 0; i < glyphInfos.Count; i++)
{
var gBounds = glyphBounds[i];
var advance = glyphInfos[i].GlyphAdvance;
runBounds = runBounds.Union(new Rect(currentX + gBounds.Left, baselineOrigin.Y + gBounds.Top, gBounds.Width, gBounds.Height));
currentX += advance;
}
ArrayPool<SKRect>.Shared.Return(glyphBounds);
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds = bounds;
Bounds = runBounds;
}
public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl;
@ -83,16 +105,12 @@ namespace Avalonia.Skia
return textBlob;
}
var font = SKFontCache.Shared.Get();
var font = _glyphTypefaceImpl.SKFont;
font.LinearMetrics = true;
font.Hinting = SKFontHinting.Full;
font.Subpixel = edging == SKFontEdging.SubpixelAntialias;
font.Edging = edging;
font.Hinting = SKFontHinting.Full;
font.Size = (float)FontRenderingEmSize;
font.Typeface = _glyphTypefaceImpl.Typeface;
font.Embolden = (_glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0;
font.SkewX = (_glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0;
var builder = SKTextBlobBuilderCache.Shared.Get();
@ -101,8 +119,6 @@ namespace Avalonia.Skia
runBuffer.SetPositions(_glyphPositions);
runBuffer.SetGlyphs(_glyphIndices);
SKFontCache.Shared.Return(font);
textBlob = builder.Build();
SKTextBlobBuilderCache.Shared.Return(builder);

44
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -9,24 +9,28 @@ namespace Avalonia.Skia
internal class GlyphTypefaceImpl : IGlyphTypeface
{
private bool _isDisposed;
private readonly SKTypeface _typeface;
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{
Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
_typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
SKFont = new SKFont(typeface)
{
LinearMetrics = true,
Embolden = (fontSimulations & FontSimulations.Bold) != 0,
SkewX = (fontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0
};
Face = new Face(GetTable)
{
UnitsPerEm = Typeface.UnitsPerEm
UnitsPerEm = typeface.UnitsPerEm
};
Font = new Font(Face);
Font.SetFunctionsOpenType();
var metrics = Typeface.ToFont().Metrics;
const double defaultFontRenderingEmSize = 12.0;
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalAscender, out var ascent);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalDescender, out var descent);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalLineGap, out var lineGap);
@ -34,10 +38,10 @@ namespace Avalonia.Skia
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughSize);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlineOffset);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize);
Metrics = new FontMetrics
{
DesignEmHeight = (short)Typeface.UnitsPerEm,
DesignEmHeight = (short)Face.UnitsPerEm,
Ascent = -ascent,
Descent = -descent,
LineGap = lineGap,
@ -45,25 +49,25 @@ namespace Avalonia.Skia
UnderlineThickness = underlineSize,
StrikethroughPosition = -strikethroughOffset,
StrikethroughThickness = strikethroughSize,
IsFixedPitch = Typeface.IsFixedPitch
IsFixedPitch = typeface.IsFixedPitch
};
GlyphCount = Typeface.GlyphCount;
GlyphCount = typeface.GlyphCount;
FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Weight = (FontWeight)typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Style = typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
Stretch = (FontStretch)typeface.FontStyle.Width;
}
public Face Face { get; }
public Font Font { get; }
public SKTypeface Typeface { get; }
public SKFont SKFont { get; }
public FontSimulations FontSimulations { get; }
@ -73,7 +77,7 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public string FamilyName => Typeface.FamilyName;
public string FamilyName => _typeface.FamilyName;
public FontWeight Weight { get; }
@ -89,7 +93,7 @@ namespace Avalonia.Skia
{
return false;
}
metrics = new GlyphMetrics
{
XBearing = extents.XBearing,
@ -97,7 +101,7 @@ namespace Avalonia.Skia
Width = extents.Width,
Height = extents.Height
};
return true;
}
@ -156,13 +160,13 @@ namespace Avalonia.Skia
private Blob? GetTable(Face face, Tag tag)
{
var size = Typeface.GetTableSize(tag);
var size = _typeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
return Typeface.TryGetTableData(tag, 0, size, data) ?
return _typeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
@ -192,7 +196,7 @@ namespace Avalonia.Skia
public bool TryGetTable(uint tag, out byte[] table)
{
return Typeface.TryGetTableData(tag, out table);
return _typeface.TryGetTableData(tag, out table);
}
}
}

11
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -77,13 +77,10 @@ namespace Avalonia.Skia
var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize;
var skFont = SKFontCache.Shared.Get();
var skFont = glyphTypeface.SKFont;
skFont.Typeface = glyphTypeface.Typeface;
skFont.Size = fontRenderingEmSize;
skFont.Edging = SKFontEdging.Alias;
skFont.Hinting = SKFontHinting.None;
skFont.LinearMetrics = true;
SKPath path = new SKPath();
@ -102,8 +99,6 @@ namespace Avalonia.Skia
currentX += glyphRun.GlyphInfos[i].GlyphAdvance;
}
SKFontCache.Shared.Return(skFont);
return new StreamGeometryImpl(path, path);
}
@ -201,9 +196,9 @@ namespace Avalonia.Skia
}
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, bounds);
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin);
}
}
}

13
src/Skia/Avalonia.Skia/SKFontCache.cs

@ -1,13 +0,0 @@
using System.Collections.Concurrent;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for SKFonts.
/// </summary>
internal class SKFontCache : SKCacheBase<SKFont, SKFontCache>
{
}
}

4
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -160,9 +160,9 @@ namespace Avalonia.Direct2D1
public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin, bounds);
return new GlyphRunImpl(glyphTypeface, fontRenderingEmSize, glyphInfos, baselineOrigin);
}
class D2DApi : IPlatformRenderInterfaceContext

9
src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs

@ -20,13 +20,12 @@ namespace Avalonia.Direct2D1.Media
private SharpDX.DirectWrite.GlyphRun? _glyphRun;
public GlyphRunImpl(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
{
_glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds = bounds;
var glyphCount = glyphInfos.Count;
@ -62,6 +61,12 @@ namespace Avalonia.Direct2D1.Media
AscenderOffset = (float)y
};
}
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
var height = glyphTypeface.Metrics.LineSpacing * scale;
Bounds = new Rect(baselineOrigin.X, 0, width, height);
}
public SharpDX.DirectWrite.GlyphRun GlyphRun

2
tests/Avalonia.RenderTests/Controls/TextBlockTests.cs

@ -121,7 +121,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 180,
Width = 190,
Height = 80,
Child = new StackPanel()

12
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@ -95,8 +95,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var fmt = Create(input, fontSize);
Assert.Equal(expWidth, fmt.Bounds.Width, 2);
Assert.Equal(expHeight, fmt.Bounds.Height, 2);
Assert.Equal(expWidth, fmt.WidthIncludingTrailingWhitespace, 2);
Assert.Equal(expHeight, fmt.Height, 2);
}
[Theory]
@ -253,8 +253,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Assert.Equal(exr.Height, r.Height, 2);
}
}
[Fact]
[Fact]
public async Task TextLayout_Basic()
{
// Skip test on OSX: text rendering is subtly different.
@ -274,7 +274,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = t.Bounds;
var textRect = new Rect(0, 0, t.WidthIncludingTrailingWhitespace, t.Height);
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
c.DrawRectangle(Brushes.Yellow, null, rect);
@ -306,7 +306,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = t.Bounds;
var textRect = new Rect(0, 0, t.WidthIncludingTrailingWhitespace, t.Height);
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
var rotate = Matrix.CreateTranslation(-100, -100) *

6
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -597,7 +597,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, layout.TextLines.Count);
Assert.Equal(lineHeight, layout.Bounds.Height);
Assert.Equal(lineHeight, layout.Height);
}
}
@ -725,7 +725,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var selectedRect = rects[0];
Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2);
Assert.Equal(selectedText.WidthIncludingTrailingWhitespace, selectedRect.Width, 2);
}
}
@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
12,
Brushes.Black);
Assert.True(layout.Bounds.Height > 0);
Assert.True(layout.Height > 0);
}
}

22
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -708,6 +708,28 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Get_CharacterHit_For_Distance_With_TextEndOfLine()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource("Hello World", defaultProperties, true);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 1000,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetCharacterHitFromDistance(1000);
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
}
}
private class MixedTextBufferTextSource : ITextSource
{
public TextRun? GetTextRun(int textSourceIndex)

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

After

Width:  |  Height:  |  Size: 773 B

BIN
tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

After

Width:  |  Height:  |  Size: 528 B

Loading…
Cancel
Save