Skip to content

Commit 87d48e8

Browse files
GillibaldMrJul
authored andcommitted
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
1 parent 065c33f commit 87d48e8

File tree

7 files changed

+192
-62
lines changed

7 files changed

+192
-62
lines changed

src/Avalonia.Base/Media/GlyphRun.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,9 +708,13 @@ private GlyphRunMetrics CreateGlyphRunMetrics()
708708
}
709709
}
710710

711+
var ascent = GlyphTypeface.Metrics.Ascent * Scale;
712+
var lineGap = GlyphTypeface.Metrics.LineGap * Scale;
713+
var baseline = -ascent + lineGap * 0.5;
714+
711715
return new GlyphRunMetrics
712716
{
713-
Baseline = -GlyphTypeface.Metrics.Ascent * Scale,
717+
Baseline = baseline,
714718
Width = width,
715719
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace,
716720
Height = height,

src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public override int Length
3636

3737
public TextMetrics TextMetrics { get; }
3838

39-
public override double Baseline => -TextMetrics.Ascent;
39+
public override double Baseline => -TextMetrics.Ascent + TextMetrics.LineGap * 0.5;
4040

4141
public override Size Size => GlyphRun.Bounds.Size;
4242

src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ private static FlowDirection GetRunDirection(TextRun? textRun, FlowDirection cur
580580
/// </returns>
581581
private int GetLastDirectionalRunIndex(int indexedRunIndex, FlowDirection flowDirection, ref double directionalWidth)
582582
{
583-
if(_indexedTextRuns is null)
583+
if (_indexedTextRuns is null)
584584
{
585585
return -1;
586586
}
@@ -624,7 +624,7 @@ private int GetLastDirectionalRunIndex(int indexedRunIndex, FlowDirection flowDi
624624

625625
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
626626
{
627-
if(textLength == 0)
627+
if (textLength == 0)
628628
{
629629
throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. ");
630630
}
@@ -643,7 +643,7 @@ public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex
643643
var indexedTextRun = _indexedTextRuns[0];
644644
var currentDirection = GetRunDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
645645

646-
return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])];
646+
return [new TextBounds(new Rect(0, 0, 0, Height), currentDirection, [])];
647647
}
648648

649649
//We can return early if the requested text range is after the line's text range.
@@ -667,7 +667,7 @@ public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex
667667
{
668668
break;
669669
}
670-
670+
671671
var currentTextRun = currentIndexedRun.TextRun;
672672

673673
if (currentTextRun == null)
@@ -691,7 +691,7 @@ public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex
691691
{
692692
directionalWidth = currentDrawable.Size.Width;
693693
}
694-
694+
695695
var firstRunIndex = currentIndexedRun.RunIndex;
696696
var lastRunIndex = GetLastDirectionalRunIndex(indexedRunIndex, currentDirection, ref directionalWidth);
697697

@@ -709,8 +709,8 @@ public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex
709709
}
710710
default:
711711
{
712-
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
713-
currentPosition, remainingLength, out coveredLength, out currentPosition);
712+
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
713+
currentPosition, remainingLength, out coveredLength, out currentPosition);
714714

715715
break;
716716
}
@@ -729,7 +729,7 @@ public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex
729729

730730
lastBounds = currentBounds;
731731

732-
if(coveredLength <= 0)
732+
if (coveredLength <= 0)
733733
{
734734
throw new InvalidOperationException("Covered length must be greater than zero.");
735735
}
@@ -988,14 +988,14 @@ private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex,
988988
{
989989
var runBounds = GetRunBounds(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition);
990990

991-
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
991+
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
992992
{
993993
textRunBounds.Add(runBounds);
994994
}
995995

996996
currentPosition = runBounds.TextSourceCharacterIndex + runBounds.Length;
997997

998-
if(i == firstRunIndex)
998+
if (i == firstRunIndex)
999999
{
10001000
startX = runBounds.Rectangle.Left;
10011001
}
@@ -1112,7 +1112,7 @@ private TextRunBounds GetRunBounds(ShapedTextRun currentRun, double currentX, in
11121112
var startHitIndex = startHit.FirstCharacterIndex;
11131113

11141114
//If the requested text range starts at the trailing edge we need to move at the end of the hit
1115-
if(startHitIndex < startIndex)
1115+
if (startHitIndex < startIndex)
11161116
{
11171117
startHitIndex += startHit.TrailingLength;
11181118
}
@@ -1230,7 +1230,7 @@ public void FinalizeLine()
12301230
}
12311231
case not null:
12321232
{
1233-
if(direction == LogicalDirection.Forward)
1233+
if (direction == LogicalDirection.Forward)
12341234
{
12351235
if (textPosition == codepointIndex)
12361236
{
@@ -1316,40 +1316,60 @@ private TextLineMetrics CreateLineMetrics()
13161316
}
13171317
}
13181318

1319-
var height = descent - ascent + lineGap;
1320-
13211319
var inkBounds = new Rect();
13221320

13231321
for (var index = 0; index < _textRuns.Length; index++)
13241322
{
13251323
switch (_textRuns[index])
13261324
{
13271325
case ShapedTextRun textRun:
1328-
{
1329-
var glyphRun = textRun.GlyphRun;
1330-
//Align the ink bounds at the common baseline
1331-
var offsetY = -ascent - textRun.Baseline;
1326+
{
1327+
var glyphRun = textRun.GlyphRun;
1328+
//Align the ink bounds at the common baseline
1329+
var offsetY = -ascent - textRun.Baseline;
13321330

1333-
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
1331+
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
13341332

1335-
inkBounds = inkBounds.Union(runBounds);
1333+
inkBounds = inkBounds.Union(runBounds);
13361334

1337-
widthIncludingWhitespace += textRun.Size.Width;
1335+
widthIncludingWhitespace += textRun.Size.Width;
13381336

1339-
break;
1340-
}
1337+
break;
1338+
}
13411339

13421340
case DrawableTextRun drawableTextRun:
1343-
{
1344-
//Align the bounds at the common baseline
1345-
var offsetY = -ascent - drawableTextRun.Baseline;
1341+
{
1342+
//Align the bounds at the common baseline
1343+
var offsetY = -ascent - drawableTextRun.Baseline;
13461344

1347-
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
1345+
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
13481346

1349-
widthIncludingWhitespace += drawableTextRun.Size.Width;
1350-
1351-
break;
1352-
}
1347+
widthIncludingWhitespace += drawableTextRun.Size.Width;
1348+
1349+
break;
1350+
}
1351+
}
1352+
}
1353+
1354+
var halfLineGap = lineGap * 0.5;
1355+
var naturalHeight = descent - ascent + lineGap;
1356+
var baseline = -ascent + halfLineGap;
1357+
var height = naturalHeight;
1358+
1359+
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
1360+
{
1361+
if (lineHeight <= naturalHeight)
1362+
{
1363+
//Clamp to the specified line height
1364+
height = lineHeight;
1365+
baseline = -ascent;
1366+
}
1367+
else
1368+
{
1369+
// Center the text vertically within the specified line height
1370+
height = lineHeight;
1371+
var extra = lineHeight - (descent - ascent);
1372+
baseline = -ascent + extra / 2;
13531373
}
13541374
}
13551375

@@ -1385,24 +1405,14 @@ private TextLineMetrics CreateLineMetrics()
13851405
}
13861406

13871407
var extent = inkBounds.Height;
1388-
//The width of overhanging pixels at the bottom
1389-
var overhangAfter = inkBounds.Bottom - height;
1408+
//The height of overhanging pixels at the bottom
1409+
var overhangAfter = inkBounds.Bottom - height + halfLineGap;
13901410
//The width of overhanging pixels at the natural alignment point. Positive value means we are inside.
13911411
var overhangLeading = inkBounds.Left;
13921412
//The width of overhanging pixels at the end of the natural bounds. Positive value means we are inside.
13931413
var overhangTrailing = widthIncludingWhitespace - inkBounds.Right;
13941414
var hasOverflowed = width > _paragraphWidth;
13951415

1396-
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
1397-
{
1398-
//Center the line
1399-
var offset = (height - lineHeight) / 2;
1400-
1401-
ascent += offset;
1402-
1403-
height = lineHeight;
1404-
}
1405-
14061416
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
14071417

14081418
_inkBounds = inkBounds.Translate(new Vector(start, 0));
@@ -1416,7 +1426,7 @@ private TextLineMetrics CreateLineMetrics()
14161426
Extent = extent,
14171427
NewlineLength = newLineLength,
14181428
Start = start,
1419-
TextBaseline = -ascent,
1429+
TextBaseline = baseline,
14201430
TrailingWhitespaceLength = trailingWhitespaceLength,
14211431
Width = width,
14221432
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,

src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
1919

2020
LineGap = fontMetrics.LineGap * scale;
2121

22+
Baseline = -Ascent + LineGap * 0.5;
23+
2224
LineHeight = Descent - Ascent + LineGap;
2325

2426
UnderlineThickness = fontMetrics.UnderlineThickness * scale;
@@ -35,6 +37,11 @@ public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
3537
/// </summary>
3638
public double FontRenderingEmSize { get; }
3739

40+
/// <summary>
41+
/// Gets the distance from the top to the baseline of the line of text.
42+
/// </summary>
43+
public double Baseline { get; }
44+
3845
/// <summary>
3946
/// Gets the recommended distance above the baseline.
4047
/// </summary>
Binary file not shown.

tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void Should_Get_Previous_CharacterHit(string text, sbyte direction)
7070
var rects = BuildRects(glyphRun);
7171

7272
rects.Reverse();
73-
73+
7474
if (glyphRun.IsLeftToRight)
7575
{
7676
foreach (var rect in rects)
@@ -95,7 +95,7 @@ public void Should_Get_Previous_CharacterHit(string text, sbyte direction)
9595
}
9696
}
9797
}
98-
98+
9999
[InlineData("ABC012345", 0)] //LeftToRight
100100
[InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן", 1)] //RightToLeft
101101
[Theory]
@@ -113,19 +113,19 @@ public void Should_Get_CharacterHit_From_Distance(string text, sbyte direction)
113113
{
114114
var characterHit =
115115
glyphRun.GetCharacterHitFromDistance(glyphRun.Bounds.Width, out _);
116-
116+
117117
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
118118
}
119119
else
120120
{
121-
var characterHit =
122-
glyphRun.GetCharacterHitFromDistance(0, out _);
123-
121+
var characterHit =
122+
glyphRun.GetCharacterHitFromDistance(0, out _);
123+
124124
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
125125
}
126-
126+
127127
var rects = BuildRects(glyphRun);
128-
128+
129129
var lastCluster = -1;
130130
var index = 0;
131131

@@ -379,20 +379,39 @@ public void Should_Get_Distance_From_CharacterHit_Within_Cluster()
379379
}
380380
}
381381

382+
[Fact]
383+
public void Should_Add_Half_LineGap_To_Baseline()
384+
{
385+
using (Start())
386+
{
387+
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Inter");
388+
var options = new TextShaperOptions(typeface.GlyphTypeface, 14);
389+
var shapedBuffer = TextShaper.Current.ShapeText("F", options);
390+
391+
var textMetrics = new TextMetrics(shapedBuffer.GlyphTypeface, 14);
392+
393+
var glyphRun = CreateGlyphRun(shapedBuffer);
394+
395+
var expectedBaseline = -textMetrics.Ascent + textMetrics.LineGap / 2;
396+
397+
Assert.Equal(expectedBaseline, glyphRun.Metrics.Baseline);
398+
}
399+
}
400+
382401
private static List<Rect> BuildRects(GlyphRun glyphRun)
383402
{
384403
var height = glyphRun.Bounds.Height;
385404

386405
var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Bounds.Width;
387-
406+
388407
var rects = new List<Rect>(glyphRun.GlyphInfos!.Count);
389408

390409
var lastCluster = -1;
391410

392411
for (var index = 0; index < glyphRun.GlyphInfos.Count; index++)
393412
{
394413
var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
395-
414+
396415
var advance = glyphRun.GlyphInfos[index].GlyphAdvance;
397416

398417
if (lastCluster != currentCluster)
@@ -412,11 +431,11 @@ private static List<Rect> BuildRects(GlyphRun glyphRun)
412431

413432
rects.Remove(rect);
414433

415-
rect = glyphRun.IsLeftToRight ?
416-
rect.WithWidth(rect.Width + advance) :
434+
rect = glyphRun.IsLeftToRight ?
435+
rect.WithWidth(rect.Width + advance) :
417436
new Rect(rect.X - advance, 0, rect.Width + advance, height);
418-
419-
rects.Add(rect);
437+
438+
rects.Add(rect);
420439
}
421440

422441
if (glyphRun.IsLeftToRight)
@@ -436,14 +455,14 @@ private static List<Rect> BuildRects(GlyphRun glyphRun)
436455

437456
private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
438457
{
439-
var glyphRun = new GlyphRun(
458+
var glyphRun = new GlyphRun(
440459
shapedBuffer.GlyphTypeface,
441460
shapedBuffer.FontRenderingEmSize,
442461
shapedBuffer.Text,
443462
shapedBuffer,
444463
biDiLevel: shapedBuffer.BidiLevel);
445464

446-
if(shapedBuffer.BidiLevel == 1)
465+
if (shapedBuffer.BidiLevel == 1)
447466
{
448467
shapedBuffer.Reverse();
449468
}

0 commit comments

Comments
 (0)