Browse Source

fix bounds and justification calculations to prevent clipping

pull/20449/head
Matthew Frederes 4 weeks ago
parent
commit
7591a0a249
  1. 45
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. 27
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  3. 21
      src/Avalonia.Controls/TextBlock.cs
  4. 16
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs

45
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
/// </summary>
/// <param name="width">The line width.</param>
/// <param name="widthIncludingTrailingWhitespace">The paragraph width including whitespace.</param>
/// <param name="trailingOverhang">The trailing overhang.</param>
/// <returns>The paragraph offset.</returns>
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;
}

27
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)

21
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)

16
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<SKRect>.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<SKRect>.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; }

Loading…
Cancel
Save