Fix overhang calculation (#19013)
* - Adjusts the overhang calculation so it matches what WPF produces - Fixes baseline alignment for inline controls * Adjust OverhangTrailing Add unit test * Only run overhang unit test on Windows
This commit is contained in:
@@ -327,7 +327,7 @@ namespace TextTestApp
|
||||
protected Size TextLineSize => _textLineSize ??= TextLine is { } textLine ? new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height) : default;
|
||||
|
||||
private Size? _inkSize;
|
||||
protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace + textLine.OverhangTrailing, textLine.Extent) : default;
|
||||
protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(-textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace - textLine.OverhangTrailing, textLine.Extent) : default;
|
||||
|
||||
public event EventHandler? TextLineChanged;
|
||||
|
||||
|
||||
@@ -691,7 +691,7 @@ namespace Avalonia.Media
|
||||
|
||||
return new GlyphRunMetrics
|
||||
{
|
||||
Baseline = (-GlyphTypeface.Metrics.Ascent + GlyphTypeface.Metrics.LineGap) * Scale,
|
||||
Baseline = -GlyphTypeface.Metrics.Ascent * Scale,
|
||||
Width = width,
|
||||
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace,
|
||||
Height = height,
|
||||
|
||||
@@ -687,8 +687,9 @@ namespace Avalonia.Media.TextFormatting
|
||||
// 4) TextWidth is the max of the text width among lines.
|
||||
// We choose to update all related metrics at once (OverhangLeading, WidthIncludingTrailingWhitespace, OverhangTrailing)
|
||||
// if the current line has a larger text width.
|
||||
var previousTextWidth = _metrics.OverhangLeading + _metrics.WidthIncludingTrailingWhitespace + _metrics.OverhangTrailing;
|
||||
var textWidth = currentLine.OverhangLeading + currentLine.WidthIncludingTrailingWhitespace + currentLine.OverhangTrailing;
|
||||
var previousTextWidth = _metrics.WidthIncludingTrailingWhitespace;
|
||||
var textWidth = currentLine.WidthIncludingTrailingWhitespace;
|
||||
|
||||
if (previousTextWidth < textWidth)
|
||||
{
|
||||
_metrics.WidthIncludingTrailingWhitespace = currentLine.WidthIncludingTrailingWhitespace;
|
||||
|
||||
@@ -88,6 +88,9 @@ namespace Avalonia.Media.TextFormatting
|
||||
/// <returns>
|
||||
/// The overhang after distance.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The value is positive if the bottommost drawn pixel goes below the line bottom, and is negative if it is within (on or above) the line.
|
||||
/// </remarks>
|
||||
public abstract double OverhangAfter { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -96,6 +99,9 @@ namespace Avalonia.Media.TextFormatting
|
||||
/// <returns>
|
||||
/// The overhang leading distance.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// When the leading drawn pixel comes before the alignment point, the value is negative.
|
||||
/// </remarks>
|
||||
public abstract double OverhangLeading { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -104,6 +110,9 @@ namespace Avalonia.Media.TextFormatting
|
||||
/// <returns>
|
||||
/// The overhang trailing distance.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The <see cref="OverhangTrailing"/> value will be positive when the trailing drawn pixel comes before the trailing alignment point.
|
||||
/// </remarks>
|
||||
public abstract double OverhangTrailing { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
/// <inheritdoc/>
|
||||
public override void Draw(DrawingContext drawingContext, Point lineOrigin)
|
||||
{
|
||||
var (currentX, currentY) = lineOrigin + new Point(Start, 0);
|
||||
var (currentX, currentY) = lineOrigin + new Point(Start, 0);
|
||||
|
||||
foreach (var textRun in _textRuns)
|
||||
{
|
||||
@@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
{
|
||||
case DrawableTextRun drawableTextRun:
|
||||
{
|
||||
var offsetY = GetBaselineOffset(drawableTextRun);
|
||||
var offsetY = GetBaselineOffset(this, drawableTextRun);
|
||||
|
||||
drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
}
|
||||
}
|
||||
|
||||
private double GetBaselineOffset(DrawableTextRun textRun)
|
||||
public static double GetBaselineOffset(TextLine textLine, DrawableTextRun textRun)
|
||||
{
|
||||
var baseline = textRun.Baseline;
|
||||
var baselineAlignment = textRun.Properties?.BaselineAlignment;
|
||||
@@ -131,19 +131,19 @@ namespace Avalonia.Media.TextFormatting
|
||||
switch (baselineAlignment)
|
||||
{
|
||||
case BaselineAlignment.Baseline:
|
||||
baselineOffset += Baseline;
|
||||
baselineOffset += textLine.Baseline;
|
||||
break;
|
||||
case BaselineAlignment.Top:
|
||||
case BaselineAlignment.TextTop:
|
||||
baselineOffset += Height - Extent + textRun.Size.Height / 2;
|
||||
baselineOffset += textLine.Height - textLine.Extent + textRun.Size.Height / 2;
|
||||
break;
|
||||
case BaselineAlignment.Center:
|
||||
baselineOffset += Height / 2 + baseline - textRun.Size.Height / 2;
|
||||
baselineOffset += textLine.Height / 2 + baseline - textRun.Size.Height / 2;
|
||||
break;
|
||||
case BaselineAlignment.Subscript:
|
||||
case BaselineAlignment.Bottom:
|
||||
case BaselineAlignment.TextBottom:
|
||||
baselineOffset += Height - textRun.Size.Height + baseline;
|
||||
baselineOffset += textLine.Height - textRun.Size.Height + baseline;
|
||||
break;
|
||||
case BaselineAlignment.Superscript:
|
||||
baselineOffset += baseline;
|
||||
@@ -1349,11 +1349,50 @@ namespace Avalonia.Media.TextFormatting
|
||||
var descent = fontMetrics.Descent * scale;
|
||||
var lineGap = fontMetrics.LineGap * scale;
|
||||
|
||||
var height = descent - ascent + lineGap;
|
||||
var lineHeight = _paragraphProperties.LineHeight;
|
||||
var lineSpacing = _paragraphProperties.LineSpacing;
|
||||
|
||||
var bounds = new Rect();
|
||||
for (var index = 0; index < _textRuns.Length; index++)
|
||||
{
|
||||
switch (_textRuns[index])
|
||||
{
|
||||
case ShapedTextRun textRun:
|
||||
{
|
||||
var textMetrics = textRun.TextMetrics;
|
||||
|
||||
if (ascent > textMetrics.Ascent)
|
||||
{
|
||||
ascent = textMetrics.Ascent;
|
||||
}
|
||||
|
||||
if (descent < textMetrics.Descent)
|
||||
{
|
||||
descent = textMetrics.Descent;
|
||||
}
|
||||
|
||||
if (lineGap < textMetrics.LineGap)
|
||||
{
|
||||
lineGap = textMetrics.LineGap;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case DrawableTextRun drawableTextRun:
|
||||
{
|
||||
if (drawableTextRun.Size.Height > -ascent)
|
||||
{
|
||||
ascent = -drawableTextRun.Size.Height;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var height = descent - ascent + lineGap;
|
||||
|
||||
var inkBounds = new Rect();
|
||||
|
||||
for (var index = 0; index < _textRuns.Length; index++)
|
||||
{
|
||||
@@ -1361,32 +1400,13 @@ namespace Avalonia.Media.TextFormatting
|
||||
{
|
||||
case ShapedTextRun textRun:
|
||||
{
|
||||
var textMetrics = textRun.TextMetrics;
|
||||
var glyphRun = textRun.GlyphRun;
|
||||
var runBounds = glyphRun.InkBounds.WithX(widthIncludingWhitespace + glyphRun.InkBounds.X);
|
||||
//Align the ink bounds at the common baseline
|
||||
var offsetY = -ascent - textRun.Baseline;
|
||||
|
||||
bounds = bounds.Union(runBounds);
|
||||
|
||||
if (ascent > textMetrics.Ascent)
|
||||
{
|
||||
ascent = textMetrics.Ascent;
|
||||
}
|
||||
|
||||
if (descent < textMetrics.Descent)
|
||||
{
|
||||
descent = textMetrics.Descent;
|
||||
}
|
||||
|
||||
if (lineGap < textMetrics.LineGap)
|
||||
{
|
||||
lineGap = textMetrics.LineGap;
|
||||
}
|
||||
|
||||
if (descent - ascent + lineGap > height)
|
||||
{
|
||||
height = descent - ascent + lineGap;
|
||||
}
|
||||
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
|
||||
|
||||
inkBounds = inkBounds.Union(runBounds);
|
||||
|
||||
widthIncludingWhitespace += textRun.Size.Width;
|
||||
|
||||
@@ -1395,26 +1415,22 @@ namespace Avalonia.Media.TextFormatting
|
||||
|
||||
case DrawableTextRun drawableTextRun:
|
||||
{
|
||||
//Align the bounds at the common baseline
|
||||
var offsetY = -ascent - drawableTextRun.Baseline;
|
||||
|
||||
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
|
||||
|
||||
widthIncludingWhitespace += drawableTextRun.Size.Width;
|
||||
|
||||
if (drawableTextRun.Size.Height > height)
|
||||
{
|
||||
height = drawableTextRun.Size.Height;
|
||||
}
|
||||
|
||||
//Adjust current ascent so drawables and text align at the bottom edge of the line.
|
||||
var offset = Math.Max(0, drawableTextRun.Baseline + ascent - descent);
|
||||
|
||||
ascent -= offset;
|
||||
|
||||
bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size));
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
height += lineSpacing;
|
||||
|
||||
var width = widthIncludingWhitespace;
|
||||
|
||||
var isRtl = _paragraphProperties.FlowDirection == FlowDirection.RightToLeft;
|
||||
|
||||
for (int i = 0; i < _textRuns.Length; i++)
|
||||
@@ -1442,16 +1458,18 @@ namespace Avalonia.Media.TextFormatting
|
||||
}
|
||||
}
|
||||
|
||||
var extent = inkBounds.Height;
|
||||
//The width of overhanging pixels at the bottom
|
||||
var overhangAfter = Math.Max(0, bounds.Bottom - height);
|
||||
//The width of overhanging pixels at the origin
|
||||
var overhangLeading = Math.Abs(Math.Min(bounds.Left, 0));
|
||||
//The width of overhanging pixels at the end
|
||||
var overhangTrailing = Math.Max(0, bounds.Right - widthIncludingWhitespace);
|
||||
var overhangAfter = inkBounds.Bottom - height;
|
||||
//The width of overhanging pixels at the natural alignment point. Positive value means we are inside.
|
||||
var overhangLeading = inkBounds.Left;
|
||||
//The width of overhanging pixels at the end of the natural bounds. Positive value means we are inside.
|
||||
var overhangTrailing = widthIncludingWhitespace - inkBounds.Right;
|
||||
var hasOverflowed = width > _paragraphWidth;
|
||||
|
||||
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
|
||||
{
|
||||
//Center the line
|
||||
var offset = (height - lineHeight) / 2;
|
||||
|
||||
ascent += offset;
|
||||
@@ -1461,15 +1479,15 @@ namespace Avalonia.Media.TextFormatting
|
||||
|
||||
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
|
||||
|
||||
_inkBounds = new Rect(bounds.Position + new Point(start, 0), bounds.Size);
|
||||
_inkBounds = inkBounds.Translate(new Vector(start, 0));
|
||||
|
||||
_bounds = new Rect(start, 0, widthIncludingWhitespace, height);
|
||||
|
||||
return new TextLineMetrics
|
||||
{
|
||||
HasOverflowed = hasOverflowed,
|
||||
Height = height + lineSpacing,
|
||||
Extent = bounds.Height,
|
||||
Height = height,
|
||||
Extent = extent,
|
||||
NewlineLength = newLineLength,
|
||||
Start = start,
|
||||
TextBaseline = -ascent,
|
||||
|
||||
@@ -18,11 +18,6 @@ namespace Avalonia.Controls.Documents
|
||||
public static readonly StyledProperty<Control> ChildProperty =
|
||||
AvaloniaProperty.Register<InlineUIContainer, Control>(nameof(Child));
|
||||
|
||||
static InlineUIContainer()
|
||||
{
|
||||
BaselineAlignmentProperty.OverrideDefaultValue<InlineUIContainer>(BaselineAlignment.Top);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of InlineUIContainer element.
|
||||
/// </summary>
|
||||
|
||||
@@ -757,10 +757,12 @@ namespace Avalonia.Controls
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var scale = 1.0;
|
||||
var padding = Padding;
|
||||
|
||||
if (UseLayoutRounding)
|
||||
{
|
||||
var scale = LayoutHelper.GetLayoutScale(this);
|
||||
scale = LayoutHelper.GetLayoutScale(this);
|
||||
padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
|
||||
}
|
||||
|
||||
@@ -796,9 +798,11 @@ namespace Avalonia.Controls
|
||||
//Fixes: #17194
|
||||
VisualChildren.Add(control);
|
||||
|
||||
var offsetY = TextLineImpl.GetBaselineOffset(textLine, drawable);
|
||||
|
||||
control.Arrange(
|
||||
new Rect(new Point(currentX, currentY),
|
||||
new Size(control.DesiredSize.Width, textLine.Height)));
|
||||
new Rect((new Point(currentX, currentY + offsetY)),
|
||||
control.DesiredSize));
|
||||
}
|
||||
|
||||
currentX += drawable.Size.Width;
|
||||
|
||||
@@ -76,14 +76,14 @@ namespace Avalonia.Skia
|
||||
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));
|
||||
runBounds = runBounds.Union(new Rect(currentX + gBounds.Left, gBounds.Top, gBounds.Width, gBounds.Height));
|
||||
|
||||
currentX += advance;
|
||||
}
|
||||
ArrayPool<SKRect>.Shared.Return(glyphBounds);
|
||||
|
||||
BaselineOrigin = baselineOrigin;
|
||||
Bounds = runBounds.Translate(new Vector(baselineOrigin.X, 0));
|
||||
Bounds = runBounds.Translate(new Vector(baselineOrigin.X, baselineOrigin.Y));
|
||||
}
|
||||
|
||||
public IGlyphTypeface GlyphTypeface => _glyphTypefaceImpl;
|
||||
|
||||
@@ -71,7 +71,6 @@ namespace Avalonia.Direct2D1.Media
|
||||
var height = metrics.Height;
|
||||
if (height < 0)
|
||||
{
|
||||
ybearing += height;
|
||||
height = -height;
|
||||
}
|
||||
|
||||
@@ -79,7 +78,9 @@ namespace Avalonia.Direct2D1.Media
|
||||
var xOffset = metrics.XBearing * scale;
|
||||
var xWidth = xOffset > 0 ? xOffset : 0;
|
||||
var xBearing = xOffset < 0 ? xOffset : 0;
|
||||
runBounds = runBounds.Union(new Rect(currentX + xBearing, baselineOrigin.Y + ybearing, xWidth + metrics.Width * scale, height * scale));
|
||||
|
||||
//yBearing is the vertical distance from the baseline to the top of the glyph's bbox. It is usually positive for horizontal layouts, and negative for vertical ones.
|
||||
runBounds = runBounds.Union(new Rect(currentX + xBearing, baselineOrigin.Y - ybearing * scale, xWidth + metrics.Width * scale, height * scale));
|
||||
}
|
||||
|
||||
currentX += glyphInfos[i].GlyphAdvance;
|
||||
|
||||
@@ -9,6 +9,7 @@ using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using Avalonia.UnitTests;
|
||||
using Xunit;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
|
||||
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
{
|
||||
@@ -1802,6 +1803,37 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
}
|
||||
}
|
||||
|
||||
[InlineData("y", -8, -1.304, -5.44)]
|
||||
[InlineData("f", -12, -11.824, -4.44)]
|
||||
[InlineData("a", 1, -0.232, -20.44)]
|
||||
[Win32Theory("Values depend on the Skia platform backend")]
|
||||
public void Should_Produce_Overhang(string text, double leading, double trailing, double after)
|
||||
{
|
||||
const string symbolsFont = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Source Serif";
|
||||
|
||||
using (Start())
|
||||
{
|
||||
var typeface = new Typeface(FontFamily.Parse(symbolsFont));
|
||||
|
||||
var defaultProperties = new GenericTextRunProperties(typeface, 64);
|
||||
|
||||
var textSource = new SingleBufferTextSource(text, defaultProperties);
|
||||
|
||||
var formatter = new TextFormatterImpl();
|
||||
|
||||
var textLine =
|
||||
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
|
||||
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
|
||||
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
|
||||
|
||||
Assert.NotNull(textLine);
|
||||
|
||||
Assert.Equal(leading, textLine.OverhangLeading, 2);
|
||||
Assert.Equal(trailing, textLine.OverhangTrailing, 2);
|
||||
Assert.Equal(after, textLine.OverhangAfter, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private class FixedRunsTextSource : ITextSource
|
||||
{
|
||||
private readonly IReadOnlyList<TextRun> _textRuns;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.Skia.UnitTests
|
||||
{
|
||||
internal class Win32Theory: TheoryAttribute
|
||||
{
|
||||
public Win32Theory(string message)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
Skip = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user