Distribute LineGap evenly to top and bottom (#19556)
* Distribute LineGap evenly to top and bottom * Add comment * Add tests * Fix line height override and add tests for it * Remove extra spaces
This commit is contained in:
@@ -708,9 +708,13 @@ namespace Avalonia.Media
|
||||
}
|
||||
}
|
||||
|
||||
var ascent = GlyphTypeface.Metrics.Ascent * Scale;
|
||||
var lineGap = GlyphTypeface.Metrics.LineGap * Scale;
|
||||
var baseline = -ascent + lineGap * 0.5;
|
||||
|
||||
return new GlyphRunMetrics
|
||||
{
|
||||
Baseline = -GlyphTypeface.Metrics.Ascent * Scale,
|
||||
Baseline = baseline,
|
||||
Width = width,
|
||||
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace,
|
||||
Height = height,
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
|
||||
public TextMetrics TextMetrics { get; }
|
||||
|
||||
public override double Baseline => -TextMetrics.Ascent;
|
||||
public override double Baseline => -TextMetrics.Ascent + TextMetrics.LineGap * 0.5;
|
||||
|
||||
public override Size Size => GlyphRun.Bounds.Size;
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
/// </returns>
|
||||
private int GetLastDirectionalRunIndex(int indexedRunIndex, FlowDirection flowDirection, ref double directionalWidth)
|
||||
{
|
||||
if(_indexedTextRuns is null)
|
||||
if (_indexedTextRuns is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -624,7 +624,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
|
||||
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
|
||||
{
|
||||
if(textLength == 0)
|
||||
if (textLength == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. ");
|
||||
}
|
||||
@@ -643,7 +643,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
var indexedTextRun = _indexedTextRuns[0];
|
||||
var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
||||
|
||||
return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])];
|
||||
return [new TextBounds(new Rect(0, 0, 0, Height), currentDirection, [])];
|
||||
}
|
||||
|
||||
//We can return early if the requested text range is after the line's text range.
|
||||
@@ -667,7 +667,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
var currentTextRun = currentIndexedRun.TextRun;
|
||||
|
||||
if (currentTextRun == null)
|
||||
@@ -691,7 +691,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
{
|
||||
directionalWidth = currentDrawable.Size.Width;
|
||||
}
|
||||
|
||||
|
||||
var firstRunIndex = currentIndexedRun.RunIndex;
|
||||
var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth);
|
||||
|
||||
@@ -709,8 +709,8 @@ namespace Avalonia.Media.TextFormatting
|
||||
}
|
||||
default:
|
||||
{
|
||||
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
|
||||
currentPosition, remainingLength, out coveredLength, out currentPosition);
|
||||
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
|
||||
currentPosition, remainingLength, out coveredLength, out currentPosition);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -729,7 +729,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
|
||||
lastBounds = currentBounds;
|
||||
|
||||
if(coveredLength <= 0)
|
||||
if (coveredLength <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Covered length must be greater than zero.");
|
||||
}
|
||||
@@ -988,14 +988,14 @@ namespace Avalonia.Media.TextFormatting
|
||||
{
|
||||
var runBounds = GetRunBounds(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition);
|
||||
|
||||
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
|
||||
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
|
||||
{
|
||||
textRunBounds.Add(runBounds);
|
||||
}
|
||||
|
||||
currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length;
|
||||
|
||||
if(i == firstRunIndex)
|
||||
if (i == firstRunIndex)
|
||||
{
|
||||
startX = runBounds.Rectangle.Left;
|
||||
}
|
||||
@@ -1112,7 +1112,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
var startHitIndex = startHit.FirstCharacterIndex;
|
||||
|
||||
//If the requested text range starts at the trailing edge we need to move at the end of the hit
|
||||
if(startHitIndex < startIndex)
|
||||
if (startHitIndex < startIndex)
|
||||
{
|
||||
startHitIndex += startHit.TrailingLength;
|
||||
}
|
||||
@@ -1230,7 +1230,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
}
|
||||
case not null:
|
||||
{
|
||||
if(direction == LogicalDirection.Forward)
|
||||
if (direction == LogicalDirection.Forward)
|
||||
{
|
||||
if (textPosition == codepointIndex)
|
||||
{
|
||||
@@ -1316,8 +1316,6 @@ namespace Avalonia.Media.TextFormatting
|
||||
}
|
||||
}
|
||||
|
||||
var height = descent - ascent + lineGap;
|
||||
|
||||
var inkBounds = new Rect();
|
||||
|
||||
for (var index = 0; index < _textRuns.Length; index++)
|
||||
@@ -1325,31 +1323,53 @@ namespace Avalonia.Media.TextFormatting
|
||||
switch (_textRuns[index])
|
||||
{
|
||||
case ShapedTextRun textRun:
|
||||
{
|
||||
var glyphRun = textRun.GlyphRun;
|
||||
//Align the ink bounds at the common baseline
|
||||
var offsetY = -ascent - textRun.Baseline;
|
||||
{
|
||||
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));
|
||||
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
|
||||
|
||||
inkBounds = inkBounds.Union(runBounds);
|
||||
inkBounds = inkBounds.Union(runBounds);
|
||||
|
||||
widthIncludingWhitespace += textRun.Size.Width;
|
||||
widthIncludingWhitespace += textRun.Size.Width;
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DrawableTextRun drawableTextRun:
|
||||
{
|
||||
//Align the bounds at the common baseline
|
||||
var offsetY = -ascent - drawableTextRun.Baseline;
|
||||
{
|
||||
//Align the bounds at the common baseline
|
||||
var offsetY = -ascent - drawableTextRun.Baseline;
|
||||
|
||||
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
|
||||
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
|
||||
|
||||
widthIncludingWhitespace += drawableTextRun.Size.Width;
|
||||
|
||||
break;
|
||||
}
|
||||
widthIncludingWhitespace += drawableTextRun.Size.Width;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var halfLineGap = lineGap * 0.5;
|
||||
var naturalHeight = descent - ascent + lineGap;
|
||||
var baseline = -ascent + halfLineGap;
|
||||
var height = naturalHeight;
|
||||
|
||||
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
|
||||
{
|
||||
if (lineHeight <= naturalHeight)
|
||||
{
|
||||
//Clamp to the specified line height
|
||||
height = lineHeight;
|
||||
baseline = -ascent;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Center the text vertically within the specified line height
|
||||
height = lineHeight;
|
||||
var extra = lineHeight - (descent - ascent);
|
||||
baseline = -ascent + extra / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,24 +1405,14 @@ namespace Avalonia.Media.TextFormatting
|
||||
}
|
||||
|
||||
var extent = inkBounds.Height;
|
||||
//The width of overhanging pixels at the bottom
|
||||
var overhangAfter = inkBounds.Bottom - 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 hasOverflowed = width > _paragraphWidth;
|
||||
|
||||
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
|
||||
{
|
||||
//Center the line
|
||||
var offset = (height - lineHeight) / 2;
|
||||
|
||||
ascent += offset;
|
||||
|
||||
height = lineHeight;
|
||||
}
|
||||
|
||||
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
|
||||
|
||||
_inkBounds = inkBounds.Translate(new Vector(start, 0));
|
||||
@@ -1416,7 +1426,7 @@ namespace Avalonia.Media.TextFormatting
|
||||
Extent = extent,
|
||||
NewlineLength = newLineLength,
|
||||
Start = start,
|
||||
TextBaseline = -ascent,
|
||||
TextBaseline = baseline,
|
||||
TrailingWhitespaceLength = trailingWhitespaceLength,
|
||||
Width = width,
|
||||
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
LineGap = fontMetrics.LineGap * scale;
|
||||
|
||||
Baseline = -Ascent + LineGap * 0.5;
|
||||
|
||||
LineHeight = Descent - Ascent + LineGap;
|
||||
|
||||
UnderlineThickness = fontMetrics.UnderlineThickness * scale;
|
||||
@@ -35,6 +37,11 @@
|
||||
/// </summary>
|
||||
public double FontRenderingEmSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the distance from the top to the baseline of the line of text.
|
||||
/// </summary>
|
||||
public double Baseline { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recommended distance above the baseline.
|
||||
/// </summary>
|
||||
|
||||
Binary file not shown.
@@ -70,7 +70,7 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
var rects = BuildRects(glyphRun);
|
||||
|
||||
rects.Reverse();
|
||||
|
||||
|
||||
if (glyphRun.IsLeftToRight)
|
||||
{
|
||||
foreach (var rect in rects)
|
||||
@@ -95,7 +95,7 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[InlineData("ABC012345", 0)] //LeftToRight
|
||||
[InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
|
||||
[Theory]
|
||||
@@ -113,19 +113,19 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
{
|
||||
var characterHit =
|
||||
glyphRun.GetCharacterHitFromDistance(glyphRun.Bounds.Width, out _);
|
||||
|
||||
|
||||
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
var characterHit =
|
||||
glyphRun.GetCharacterHitFromDistance(0, out _);
|
||||
|
||||
var characterHit =
|
||||
glyphRun.GetCharacterHitFromDistance(0, out _);
|
||||
|
||||
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
|
||||
}
|
||||
|
||||
|
||||
var rects = BuildRects(glyphRun);
|
||||
|
||||
|
||||
var lastCluster = -1;
|
||||
var index = 0;
|
||||
|
||||
@@ -379,12 +379,31 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Add_Half_LineGap_To_Baseline()
|
||||
{
|
||||
using (Start())
|
||||
{
|
||||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
|
||||
var options = new TextShaperOptions(typeface.GlyphTypeface, 14);
|
||||
var shapedBuffer = TextShaper.Current.ShapeText("F", options);
|
||||
|
||||
var textMetrics = new TextMetrics(shapedBuffer.GlyphTypeface, 14);
|
||||
|
||||
var glyphRun = CreateGlyphRun(shapedBuffer);
|
||||
|
||||
var expectedBaseline = -textMetrics.Ascent + textMetrics.LineGap / 2;
|
||||
|
||||
Assert.Equal(expectedBaseline, glyphRun.Metrics.Baseline);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Rect> BuildRects(GlyphRun glyphRun)
|
||||
{
|
||||
var height = glyphRun.Bounds.Height;
|
||||
|
||||
var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Bounds.Width;
|
||||
|
||||
|
||||
var rects = new List<Rect>(glyphRun.GlyphInfos!.Count);
|
||||
|
||||
var lastCluster = -1;
|
||||
@@ -392,7 +411,7 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
for (var index = 0; index < glyphRun.GlyphInfos.Count; index++)
|
||||
{
|
||||
var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
|
||||
|
||||
|
||||
var advance = glyphRun.GlyphInfos[index].GlyphAdvance;
|
||||
|
||||
if (lastCluster != currentCluster)
|
||||
@@ -412,11 +431,11 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
|
||||
rects.Remove(rect);
|
||||
|
||||
rect = glyphRun.IsLeftToRight ?
|
||||
rect.WithWidth(rect.Width + advance) :
|
||||
rect = glyphRun.IsLeftToRight ?
|
||||
rect.WithWidth(rect.Width + advance) :
|
||||
new Rect(rect.X - advance, 0, rect.Width + advance, height);
|
||||
|
||||
rects.Add(rect);
|
||||
|
||||
rects.Add(rect);
|
||||
}
|
||||
|
||||
if (glyphRun.IsLeftToRight)
|
||||
@@ -436,14 +455,14 @@ namespace Avalonia.Skia.UnitTests.Media
|
||||
|
||||
private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
|
||||
{
|
||||
var glyphRun = new GlyphRun(
|
||||
var glyphRun = new GlyphRun(
|
||||
shapedBuffer.GlyphTypeface,
|
||||
shapedBuffer.FontRenderingEmSize,
|
||||
shapedBuffer.Text,
|
||||
shapedBuffer,
|
||||
biDiLevel: shapedBuffer.BidiLevel);
|
||||
|
||||
if(shapedBuffer.BidiLevel == 1)
|
||||
if (shapedBuffer.BidiLevel == 1)
|
||||
{
|
||||
shapedBuffer.Reverse();
|
||||
}
|
||||
|
||||
@@ -2195,6 +2195,96 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Add_Half_LineGap_To_Baseline()
|
||||
{
|
||||
using (Start())
|
||||
{
|
||||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
|
||||
var defaultProperties = new GenericTextRunProperties(typeface);
|
||||
|
||||
var textSource = new SingleBufferTextSource("F", defaultProperties);
|
||||
|
||||
var formatter = new TextFormatterImpl();
|
||||
|
||||
var textLine =
|
||||
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
|
||||
new GenericTextParagraphProperties(defaultProperties));
|
||||
|
||||
Assert.NotNull(textLine);
|
||||
|
||||
var textMetrics = new TextMetrics(typeface.GlyphTypeface, 12);
|
||||
|
||||
var expectedBaseline = -textMetrics.Ascent + textMetrics.LineGap / 2;
|
||||
|
||||
Assert.Equal(expectedBaseline, textLine.Baseline);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Clamp_Baseline_When_LineHeight_Is_Smaller_Than_Natural()
|
||||
{
|
||||
using (Start())
|
||||
{
|
||||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
|
||||
var defaultProperties = new GenericTextRunProperties(typeface);
|
||||
|
||||
var textSource = new SingleBufferTextSource("F", defaultProperties);
|
||||
|
||||
var formatter = new TextFormatterImpl();
|
||||
|
||||
var textMetrics = new TextMetrics(typeface.GlyphTypeface, 12);
|
||||
var natural = -textMetrics.Ascent + textMetrics.Descent + textMetrics.LineGap;
|
||||
|
||||
var smallerLineHeight = natural - 2;
|
||||
|
||||
// Force a smaller line height than ascent+descent+lineGap
|
||||
var paragraphProps = new GenericTextParagraphProperties(defaultProperties, lineHeight: smallerLineHeight);
|
||||
|
||||
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProps);
|
||||
|
||||
Assert.NotNull(textLine);
|
||||
|
||||
// In this case, baseline should equal -Ascent (lineGap ignored)
|
||||
var expectedBaseline = -textMetrics.Ascent;
|
||||
|
||||
Assert.Equal(expectedBaseline, textLine.Baseline);
|
||||
Assert.Equal(paragraphProps.LineHeight, textLine.Height);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Distribute_Extra_Space_When_LineHeight_Is_Larger_Than_Natural()
|
||||
{
|
||||
using (Start())
|
||||
{
|
||||
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
|
||||
var defaultProperties = new GenericTextRunProperties(typeface);
|
||||
|
||||
var textSource = new SingleBufferTextSource("F", defaultProperties);
|
||||
|
||||
var formatter = new TextFormatterImpl();
|
||||
|
||||
var textMetrics = new TextMetrics(typeface.GlyphTypeface, 12);
|
||||
var natural = -textMetrics.Ascent + textMetrics.Descent + textMetrics.LineGap;
|
||||
|
||||
var largerLineHeight = natural + 50;
|
||||
|
||||
var paragraphProps = new GenericTextParagraphProperties(defaultProperties, lineHeight: largerLineHeight);
|
||||
|
||||
var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProps);
|
||||
|
||||
Assert.NotNull(textLine);
|
||||
|
||||
// Extra space is distributed evenly above and below
|
||||
var extra = largerLineHeight - (textMetrics.Descent - textMetrics.Ascent);
|
||||
var expectedBaseline = -textMetrics.Ascent + extra / 2;
|
||||
|
||||
Assert.Equal(expectedBaseline, textLine.Baseline, 5);
|
||||
Assert.Equal(largerLineHeight, textLine.Height, 5);
|
||||
}
|
||||
}
|
||||
|
||||
private class FixedRunsTextSource : ITextSource
|
||||
{
|
||||
private readonly IReadOnlyList<TextRun> _textRuns;
|
||||
|
||||
Reference in New Issue
Block a user