diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 417dfc77fa..1d4793e85d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1328,7 +1328,7 @@ namespace Avalonia.Media.TextFormatting } } - var inkBounds = new Rect(); + Rect? inkBounds = null; for (var index = 0; index < _textRuns.Length; index++) { @@ -1342,7 +1342,14 @@ namespace Avalonia.Media.TextFormatting var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY)); - inkBounds = inkBounds.Union(runBounds); + if (inkBounds == null) + { + inkBounds = runBounds; + } + else + { + inkBounds = inkBounds.Value.Union(runBounds); + } widthIncludingWhitespace += textRun.Size.Width; @@ -1354,7 +1361,16 @@ namespace Avalonia.Media.TextFormatting //Align the bounds at the common baseline var offsetY = -ascent - drawableTextRun.Baseline; - inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size)); + var drawableBounds = new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size); + + if (inkBounds == null) + { + inkBounds = drawableBounds; + } + else + { + inkBounds = inkBounds.Value.Union(drawableBounds); + } widthIncludingWhitespace += drawableTextRun.Size.Width; @@ -1362,6 +1378,8 @@ namespace Avalonia.Media.TextFormatting } } } + + var finalInkBounds = inkBounds ?? new Rect(); var halfLineGap = lineGap * 0.5; var naturalHeight = descent - ascent + lineGap; @@ -1416,18 +1434,15 @@ namespace Avalonia.Media.TextFormatting } } - var extent = inkBounds.Height; - //The height of overhanging pixels at the bottom - var overhangAfter = inkBounds.Bottom - height + halfLineGap; - //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 extent = finalInkBounds.Height; + var overhangAfter = finalInkBounds.Bottom - height + halfLineGap; + var overhangLeading = finalInkBounds.Left; + var overhangTrailing = widthIncludingWhitespace - finalInkBounds.Right; var hasOverflowed = width > _paragraphWidth; - var start = GetParagraphOffsetX(width, widthIncludingWhitespace); + var start = GetParagraphOffsetX(width, widthIncludingWhitespace, overhangTrailing); - _inkBounds = inkBounds.Translate(new Vector(start, 0)); + _inkBounds = finalInkBounds.Translate(new Vector(start, 0)); _bounds = new Rect(start, 0, widthIncludingWhitespace, height); @@ -1453,9 +1468,10 @@ namespace Avalonia.Media.TextFormatting /// /// The line width. /// The paragraph width including whitespace. + /// The trailing overhang. /// The paragraph offset. - private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace) + private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace, double trailingOverhang) { if (double.IsPositiveInfinity(_paragraphWidth)) { @@ -1501,7 +1517,8 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, start); case TextAlignment.Right: - return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); + var overhangAdjustment = Math.Min(0, trailingOverhang); + return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace + overhangAdjustment); default: return 0; } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 7078650da4..8eccb0b219 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -372,6 +372,11 @@ namespace Avalonia.Controls.Presenters var top = 0d; var left = 0.0; + + foreach (var line in TextLayout.TextLines) + { + left = Math.Max(left, -Math.Min(0, line.OverhangLeading)); + } var textHeight = TextLayout.Height; @@ -627,8 +632,16 @@ namespace Avalonia.Controls.Presenters InvalidateArrange(); - // The textWidth used here is matching that TextBlock uses to measure the text. - var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing; + var textWidth = TextLayout.WidthIncludingTrailingWhitespace; + + foreach (var line in TextLayout.TextLines) + { + var lineInkWidth = line.WidthIncludingTrailingWhitespace + - Math.Min(0, line.OverhangLeading) + - Math.Min(0, line.OverhangTrailing); + textWidth = Math.Max(textWidth, lineInkWidth); + } + return new Size(textWidth, TextLayout.Height); } @@ -636,7 +649,15 @@ namespace Avalonia.Controls.Presenters { var finalWidth = finalSize.Width; - var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing; + var textWidth = TextLayout.WidthIncludingTrailingWhitespace; + + foreach (var line in TextLayout.TextLines) + { + var lineInkWidth = line.WidthIncludingTrailingWhitespace + - Math.Min(0, line.OverhangLeading) + - Math.Min(0, line.OverhangTrailing); + textWidth = Math.Max(textWidth, lineInkWidth); + } textWidth = Math.Ceiling(textWidth); if (finalSize.Width < textWidth) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 55cbae773c..22e60efcdc 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -630,7 +630,13 @@ namespace Avalonia.Controls protected virtual void RenderTextLayout(DrawingContext context, Point origin) { - TextLayout.Draw(context, origin); + var maxLeadingOverhang = 0.0; + foreach (var line in TextLayout.TextLines) + { + maxLeadingOverhang = Math.Max(maxLeadingOverhang, -Math.Min(0, line.OverhangLeading)); + } + + TextLayout.Draw(context, new Point(origin.X + maxLeadingOverhang, origin.Y)); } private bool _clearTextInternal; @@ -748,8 +754,17 @@ namespace Avalonia.Controls //This implicitly recreated the TextLayout with a new constraint if we previously reset it. var textLayout = TextLayout; - // The textWidth used here is matching that TextPresenter uses to measure the text. - return new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height).Inflate(padding); + var totalWidth = textLayout.WidthIncludingTrailingWhitespace; + + foreach (var line in textLayout.TextLines) + { + var lineInkWidth = line.WidthIncludingTrailingWhitespace + - Math.Min(0, line.OverhangLeading) + - Math.Min(0, line.OverhangTrailing); + totalWidth = Math.Max(totalWidth, lineInkWidth); + } + + return new Size(totalWidth, textLayout.Height).Inflate(padding); } protected override Size ArrangeOverride(Size finalSize) diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 30f6da1dc2..7eee5d9fdc 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -64,26 +64,36 @@ namespace Avalonia.Skia // "F" text with Inter size 14 has a 0px left bound with SubpixelAntialias but 1px with Antialias. using var font = CreateFont(SKFontEdging.SubpixelAntialias); - var runBounds = new Rect(); var glyphBounds = ArrayPool.Shared.Rent(count); font.GetGlyphWidths(_glyphIndices, null, glyphBounds.AsSpan(0, count)); currentX = 0; + + Rect? runBounds = null; for (var i = 0; i < count; i++) { var gBounds = glyphBounds[i]; var advance = glyphInfos[i].GlyphAdvance; - runBounds = runBounds.Union(new Rect(currentX + gBounds.Left, gBounds.Top, gBounds.Width, gBounds.Height)); + var glyphRect = new Rect(currentX + gBounds.Left, gBounds.Top, gBounds.Width, gBounds.Height); + + if (runBounds == null) + { + runBounds = glyphRect; + } + else + { + runBounds = runBounds.Value.Union(glyphRect); + } currentX += advance; } ArrayPool.Shared.Return(glyphBounds); BaselineOrigin = baselineOrigin; - Bounds = runBounds.Translate(new Vector(baselineOrigin.X, baselineOrigin.Y)); + Bounds = (runBounds ?? new Rect()).Translate(new Vector(baselineOrigin.X, baselineOrigin.Y)); } public double FontRenderingEmSize { get; }