diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index a81dbdb3f0..f8e0eb88bd 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -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, diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 34dc8e8420..3a578fb72d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -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; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index 3cb26882dc..9482b6440b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -88,6 +88,9 @@ namespace Avalonia.Media.TextFormatting /// /// The overhang after distance. /// + /// + /// 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. + /// public abstract double OverhangAfter { get; } /// @@ -96,6 +99,9 @@ namespace Avalonia.Media.TextFormatting /// /// The overhang leading distance. /// + /// + /// When the leading drawn pixel comes before the alignment point, the value is negative. + /// public abstract double OverhangLeading { get; } /// @@ -104,6 +110,9 @@ namespace Avalonia.Media.TextFormatting /// /// The overhang trailing distance. /// + /// + /// The value will be positive when the trailing drawn pixel comes before the trailing alignment point. + /// public abstract double OverhangTrailing { get; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 3a4e83663a..6e7e39fe59 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -101,7 +101,7 @@ namespace Avalonia.Media.TextFormatting /// 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,44 +1349,64 @@ 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; - var glyphRun = textRun.GlyphRun; - var runBounds = glyphRun.InkBounds.WithX(widthIncludingWhitespace + glyphRun.InkBounds.X); + { + var textMetrics = textRun.TextMetrics; - bounds = bounds.Union(runBounds); + if (ascent > textMetrics.Ascent) + { + ascent = textMetrics.Ascent; + } - if (ascent > textMetrics.Ascent) - { - ascent = textMetrics.Ascent; - } + if (descent < textMetrics.Descent) + { + descent = textMetrics.Descent; + } - if (descent < textMetrics.Descent) - { - descent = textMetrics.Descent; - } + if (lineGap < textMetrics.LineGap) + { + lineGap = textMetrics.LineGap; + } - if (lineGap < textMetrics.LineGap) - { - lineGap = textMetrics.LineGap; + break; } - if (descent - ascent + lineGap > height) + case DrawableTextRun drawableTextRun: { - height = descent - ascent + lineGap; + 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++) + { + switch (_textRuns[index]) + { + case ShapedTextRun textRun: + { + var glyphRun = textRun.GlyphRun; + //Align the ink bounds at the common baseline + var offsetY = -ascent - textRun.Baseline; + + 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: { - widthIncludingWhitespace += drawableTextRun.Size.Width; - - if (drawableTextRun.Size.Height > height) - { - height = drawableTextRun.Size.Height; - } + //Align the bounds at the common baseline + var offsetY = -ascent - drawableTextRun.Baseline; - //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)); + inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size)); + widthIncludingWhitespace += drawableTextRun.Size.Width; + 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, diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index 7f5aa55959..4d9e776bbf 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -18,11 +18,6 @@ namespace Avalonia.Controls.Documents public static readonly StyledProperty ChildProperty = AvaloniaProperty.Register(nameof(Child)); - static InlineUIContainer() - { - BaselineAlignmentProperty.OverrideDefaultValue(BaselineAlignment.Top); - } - /// /// Initializes a new instance of InlineUIContainer element. /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 4067afb166..95f075d71d 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -748,8 +748,14 @@ namespace Avalonia.Controls protected override Size ArrangeOverride(Size finalSize) { - var scale = LayoutHelper.GetLayoutScale(this); - var padding = LayoutHelper.RoundLayoutThickness(Padding, scale); + var scale = 1.0; + var padding = Padding; + + if (UseLayoutRounding) + { + scale = LayoutHelper.GetLayoutScale(this); + padding = LayoutHelper.RoundLayoutThickness(Padding, scale); + } var availableSize = finalSize.Deflate(padding); @@ -783,9 +789,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; diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 29b8a8c1a1..0cc069308f 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -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.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; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 7f8282e4ed..729797203e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -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; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index b690386422..ae74c76194 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -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 _textRuns; diff --git a/tests/Avalonia.Skia.UnitTests/Win32Theory.cs b/tests/Avalonia.Skia.UnitTests/Win32Theory.cs new file mode 100644 index 0000000000..afab42e8ee --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Win32Theory.cs @@ -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; + } + } +}