Browse Source

TextLine.GetTextBounds zero width run fixes (#19602)

* Rework GetRunBounds

* Reduce duplication

* Update tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

Co-authored-by: Jan Kučera <10546952+miloush@users.noreply.github.com>

---------

Co-authored-by: Jan Kučera <10546952+miloush@users.noreply.github.com>
pull/19616/head
Benedikt Stebner 5 months ago
committed by GitHub
parent
commit
b92bb933ac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 130
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. 36
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

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

@ -916,7 +916,7 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is ShapedTextRun shapedTextRun) if (currentRun is ShapedTextRun shapedTextRun)
{ {
var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition); var runBounds = GetRunBounds(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition);
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
{ {
@ -996,7 +996,7 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is ShapedTextRun shapedTextRun) if (currentRun is ShapedTextRun shapedTextRun)
{ {
var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition); var runBounds = GetRunBounds(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition);
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length) if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
{ {
@ -1062,12 +1062,15 @@ namespace Avalonia.Media.TextFormatting
return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds); return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
} }
private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX, private TextRunBounds GetRunBounds(ShapedTextRun currentRun, double currentX, int firstTextSourceIndex, int remainingLength, int currentPosition)
int firstTextSourceIndex, int remainingLength, int currentPosition)
{ {
//Determine the start of the first hit in local positions. bool isLeftToRight = currentRun.BidiLevel % 2 == 0;
double startX = currentX;
double endX = currentX;
// Determine the start of the first hit in local positions
var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition); var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition);
var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
//The start index needs to be relative to the first cluster //The start index needs to be relative to the first cluster
@ -1081,15 +1084,13 @@ namespace Avalonia.Media.TextFormatting
var clusterOffset = 0; var clusterOffset = 0;
//Cluster boundary correction // Cluster boundary correction
if (runOffset > 0) if (runOffset > 0)
{ {
var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _); var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _);
var clusterStart = characterHit.FirstCharacterIndex; var clusterStart = characterHit.FirstCharacterIndex;
var clusterEnd = clusterStart + characterHit.TrailingLength; var clusterEnd = clusterStart + characterHit.TrailingLength;
//Test against left and right edge
if (clusterStart < startIndex && clusterEnd > startIndex) if (clusterStart < startIndex && clusterEnd > startIndex)
{ {
//Remember the cluster correction offset //Remember the cluster correction offset
@ -1100,106 +1101,43 @@ namespace Avalonia.Media.TextFormatting
} }
} }
//Find the visual start and end position we want to hit test against //Find the visual start and end position of the hit
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex)); var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex));
// Preserve non-zero width for zero-advance ranges if (isLeftToRight)
if (startOffset == endOffset && startIndex != endIndex)
{ {
//We need to make sure a zero width text line is hit test at the end so we add some delta endX = startX + endOffset;
endOffset += MathUtilities.DoubleEpsilon; startX += startOffset;
} }
else
var endX = startX + endOffset;
startX += startOffset;
//Hit test against visual positions
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength;
var endHitIndex = endHit.FirstCharacterIndex + endHit.TrailingLength;
//Adjust characterLength by the cluster offset to only cover the remaining length of the cluster.
var characterLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset);
// Normalize bounds
if (endX < startX)
{ {
(endX, startX) = (startX, endX); //We need the distance from right to left and GetDistanceFromCharacterHit returs a distance from left to right so we need to adjust the offsets
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
} }
var runWidth = endX - startX; // Find the start of the hit
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
//We need to adjust the local position to the text source var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength;
var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
}
private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int remainingLength, int currentPosition)
{
// We start from the right edge of the run
var startX = endX;
//Determine the start of the first hit in local positions.
var runOffset = Math.Max(0, firstTextSourceIndex - currentPosition);
var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
//The start index needs to be relative to the first cluster
var startIndex = firstCluster + runOffset;
var endIndex = startIndex + remainingLength;
//Current position is a text source index and first cluster is relative to the GlyphRun's buffer. //Find the next possible position that contains the endIndex
var textSourceOffset = currentPosition - firstCluster; var nearestCharacterHit = currentRun.GlyphRun.FindNearestCharacterHit(endIndex, out _);
Debug.Assert(textSourceOffset >= 0);
var clusterOffset = 0; int endHitIndex;
//Cluster boundary correction if (nearestCharacterHit.FirstCharacterIndex < endIndex)
if (runOffset > 0)
{ {
var characterHit = currentRun.GlyphRun.FindNearestCharacterHit(startIndex, out _); //The hit is inside or at the trailing edge
endHitIndex = nearestCharacterHit.FirstCharacterIndex + nearestCharacterHit.TrailingLength;
var clusterStart = characterHit.FirstCharacterIndex;
var clusterEnd = clusterStart + characterHit.TrailingLength;
//Test against left and right edge
if (clusterStart < startIndex && clusterEnd > startIndex)
{
//Remember the cluster correction offset
clusterOffset = startIndex - clusterStart;
//Move to the start of the cluster
startIndex -= clusterOffset;
}
} }
else
//Find the visual start and end position we want to hit test against
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(endIndex));
//We need the distance from right to left and GetDistanceFromCharacterHit returs a distance from left to right so we need to adjust the offsets
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
// Preserve non-zero width for zero-advance ranges
if (startOffset == endOffset && startIndex != endIndex)
{ {
//We need to make sure a zero width text line is hit test at the end so we add some delta //The hit is at the leading edge
endOffset += MathUtilities.DoubleEpsilon; endHitIndex = nearestCharacterHit.FirstCharacterIndex;
} }
//Hit test against visual positions var coveredLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset);
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHitIndex = startHit.FirstCharacterIndex + startHit.TrailingLength;
var endHitIndex = endHit.FirstCharacterIndex + endHit.TrailingLength;
var characterLength = Math.Max(0, Math.Abs(startHitIndex - endHitIndex) - clusterOffset);
// Normalize bounds // Normalize bounds
if (endX < startX) if (endX < startX)
@ -1212,7 +1150,7 @@ namespace Avalonia.Media.TextFormatting
//We need to adjust the local position to the text source //We need to adjust the local position to the text source
var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset; var textSourceIndex = textSourceOffset + startHitIndex + clusterOffset;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun); return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, coveredLength, currentRun);
} }
public override void Dispose() public override void Dispose()

36
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -2011,6 +2011,42 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_Get_TextBounds_With_Glue()
{
using (Start())
{
var typeface = Typeface.Default;
var defaultProperties = new GenericTextRunProperties(typeface);
var text = "a\u202C\u202C\u202C\u202Cb";
var shaperOption = new TextShaperOptions(typeface.GlyphTypeface);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var textBounds = textLine.GetTextBounds(1, 1);
Assert.NotEmpty(textBounds);
var firstTextBounds = textBounds[0];
Assert.NotEmpty(firstTextBounds.TextRunBounds);
var firstRunBounds = firstTextBounds.TextRunBounds[0];
Assert.Equal(1, firstRunBounds.TextSourceCharacterIndex);
Assert.Equal(1, firstRunBounds.Length);
}
}
[Fact] [Fact]
public void Should_Get_TextBounds_Tamil() public void Should_Get_TextBounds_Tamil()
{ {

Loading…
Cancel
Save