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