Browse Source

Merge pull request #8094 from Gillibald/fixes/textProcessingBugs

Text processing fixes
pull/8233/head
Max Katz 4 years ago
committed by GitHub
parent
commit
0ed9ef288e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
  2. 29
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  3. 376
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  4. 39
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  5. 4
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  6. 5
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  7. 6
      src/Avalonia.Controls/TextBlock.cs
  8. 2
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  9. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  10. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  11. 184
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  12. 2
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  13. 2
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

11
src/Avalonia.Base/Media/TextFormatting/TextBounds.cs

@ -10,20 +10,27 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Constructing TextBounds object
/// </summary>
internal TextBounds(Rect bounds, FlowDirection flowDirection)
internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{
Rectangle = bounds;
FlowDirection = flowDirection;
TextRunBounds = runBounds;
}
/// <summary>
/// Bounds rectangle
/// </summary>
public Rect Rectangle { get; }
public Rect Rectangle { get; internal set; }
/// <summary>
/// Text flow direction inside the boundary rectangle
/// </summary>
public FlowDirection FlowDirection { get; }
/// <summary>
/// Get a list of run bounding rectangles
/// </summary>
/// <returns>Array of text run bounds</returns>
public IList<TextRunBounds> TextRunBounds { get; }
}
}

29
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -230,7 +230,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in TextLines)
{
//Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length <= start)
if (textLine.FirstTextSourceIndex + textLine.Length < start)
{
currentY += textLine.Height;
@ -239,18 +239,27 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length);
foreach (var bounds in textBounds)
if(textBounds.Count > 0)
{
Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
foreach (var bounds in textBounds)
{
result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
{
result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
}
else
{
result.Add(bounds.Rectangle.WithY(currentY));
}
foreach (var runBounds in bounds.TextRunBounds)
{
start += runBounds.Length;
length -= runBounds.Length;
}
}
else
{
result.Add(bounds.Rectangle.WithY(currentY));
}
}
if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)

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

@ -184,6 +184,10 @@ namespace Avalonia.Media.TextFormatting
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
break;
}
default:
@ -215,9 +219,11 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
var isTrailingHit = characterHit.TrailingLength > 0;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var currentDistance = Start;
var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
GlyphRun? lastRun = null;
@ -242,8 +248,10 @@ namespace Avalonia.Media.TextFormatting
}
//Look for a hit in within the current run
if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.Start + textRun.Text.Length)
if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length)
{
characterHit = new CharacterHit(textRun.Text.Start + remainingLength);
var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
return currentDistance + distance;
@ -254,28 +262,27 @@ namespace Avalonia.Media.TextFormatting
{
if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{
if (characterIndex <= textRun.Text.Start)
if (characterIndex <= currentPosition)
{
return currentDistance;
}
}
else
{
if (characterIndex == textRun.Text.Start)
if (characterIndex == currentPosition)
{
return currentDistance;
}
}
if (characterIndex == textRun.Text.Start + textRun.Text.Length &&
characterHit.TrailingLength > 0)
if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit)
{
return currentDistance + currentRun.Size.Width;
}
}
else
{
if (characterIndex == textRun.Text.Start)
if (characterIndex == currentPosition)
{
return currentDistance + currentRun.Size.Width;
}
@ -286,20 +293,24 @@ namespace Avalonia.Media.TextFormatting
if (nextRun != null)
{
if (characterHit.FirstCharacterIndex == textRun.Text.End &&
nextRun.ShapedBuffer.IsLeftToRight)
if (nextRun.ShapedBuffer.IsLeftToRight)
{
return currentDistance;
if (characterIndex == currentPosition + textRun.Text.Length)
{
return currentDistance;
}
}
if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
else
{
return currentDistance;
if (currentPosition + nextRun.Text.Length == characterIndex)
{
return currentDistance;
}
}
}
else
{
if (characterIndex > textRun.Text.End)
if (characterIndex > currentPosition + textRun.Text.Length)
{
return currentDistance;
}
@ -329,6 +340,12 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width
currentDistance += textRun.Size.Width;
currentPosition += textRun.TextSourceLength;
remainingLength -= textRun.TextSourceLength;
if (remainingLength <= 0)
{
break;
}
}
return currentDistance;
@ -394,210 +411,299 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
private IReadOnlyList<TextBounds> GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
{
if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
{
return Array.Empty<TextBounds>();
}
var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(TextRuns.Count);
var lastDirection = _flowDirection;
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
var currentPosition = FirstTextSourceIndex;
var currentRect = Rect.Empty;
var remainingLength = textLength;
var startX = Start;
double currentWidth = 0;
var currentRect = Rect.Empty;
//A portion of the line is covered.
for (var index = 0; index < TextRuns.Count; index++)
{
var currentRun = TextRuns[index] as DrawableTextRun;
if (currentRun is null)
if (TextRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
TextRun? nextRun = null;
if (index + 1 < TextRuns.Count)
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
nextRun = TextRuns[index + 1];
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (nextRun != null)
var characterLength = 0;
var endX = startX;
if (currentRun is ShapedTextCharacters currentShapedRun)
{
switch (nextRun)
{
case ShapedTextCharacters when currentRun is ShapedTextCharacters:
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
{
goto skip;
}
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
goto skip;
}
currentPosition += offset;
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
{
goto skip;
}
var startIndex = currentRun.Text.Start + offset;
if (currentRun.Text.End < firstTextSourceCharacterIndex)
{
goto skip;
}
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
goto noop;
}
default:
{
goto noop;
}
}
endX += endOffset;
var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex) :
new CharacterHit(startIndex + remainingLength));
startX += startOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
skip:
currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
else
{
if (currentPosition < firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
}
continue;
noop:
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX += currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
}
}
var endX = startX;
var endOffset = 0d;
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
switch (currentRun)
//Lines that only contain a linebreak need to be covered here
if(characterLength == 0)
{
case ShapedTextCharacters shapedRun:
{
endOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
shapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex + textLength) :
new CharacterHit(firstTextSourceCharacterIndex));
characterLength = NewLineLength;
}
endX += endOffset;
var runwidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun);
var startOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
shapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex) :
new CharacterHit(firstTextSourceCharacterIndex + textLength));
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentWidth + runwidth);
startX += startOffset;
var textBounds = result[result.Count - 1];
var characterHit = shapedRun.GlyphRun.IsLeftToRight ?
shapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
shapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
textBounds.Rectangle = currentRect;
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
currentDirection = shapedRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
if (nextRun is ShapedTextCharacters nextShaped)
{
if (shapedRun.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight)
{
endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit(
nextShaped.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex + textLength) :
new CharacterHit(firstTextSourceCharacterIndex));
currentWidth += runwidth;
currentPosition += characterLength;
index++;
if (currentDirection == FlowDirection.LeftToRight)
{
if (currentPosition > characterIndex)
{
break;
}
}
else
{
if (currentPosition <= firstTextSourceIndex)
{
break;
}
}
endX += endOffset;
startX = endX;
lastDirection = currentDirection;
remainingLength -= characterLength;
currentRun = nextShaped;
if (remainingLength <= 0)
{
break;
}
}
if (nextShaped.ShapedBuffer.IsLeftToRight)
{
characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
return result;
}
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
}
}
}
private IReadOnlyList<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
{
var characterIndex = firstTextSourceIndex + textLength;
break;
}
default:
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
{
endX += currentRun.Size.Width;
}
var result = new List<TextBounds>(TextRuns.Count);
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
if (currentPosition < firstTextSourceCharacterIndex)
{
startX += currentRun.Size.Width;
}
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
currentPosition += currentRun.TextSourceLength;
var startX = Start + WidthIncludingTrailingWhitespace;
double currentWidth = 0;
var currentRect = Rect.Empty;
break;
}
for (var index = TextRuns.Count - 1; index >= 0; index--)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
if (endX < startX)
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
(endX, startX) = (startX, endX);
startX -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
var width = endX - startX;
var characterLength = 0;
var endX = startX;
if (!MathUtilities.IsZero(width))
if (currentRun is ShapedTextCharacters currentShapedRun)
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentRect.Width + width);
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
endX += endOffset - currentShapedRun.Size.Width;
var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex) :
new CharacterHit(startIndex + remainingLength));
var textBounds = new TextBounds(currentRect, currentDirection);
startX += startOffset - currentShapedRun.Size.Width;
result[result.Count - 1] = textBounds;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
FlowDirection.LeftToRight :
FlowDirection.RightToLeft;
}
else
{
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX -= currentRun.Size.Width;
}
else
if (currentPosition < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
}
}
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
currentRect = new Rect(startX, 0, width, Height);
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
result.Add(new TextBounds(currentRect, currentDirection));
var runWidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
var textBounds = result[result.Count - 1];
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
currentWidth += runWidth;
currentPosition += characterLength;
if (currentDirection == FlowDirection.LeftToRight)
{
if (currentPosition > firstTextSourceCharacterIndex + textLength)
if (currentPosition > characterIndex)
{
break;
}
}
else
{
if (currentPosition <= firstTextSourceCharacterIndex)
if (currentPosition <= firstTextSourceIndex)
{
break;
}
endX += currentRun.Size.Width - endOffset;
}
lastDirection = currentDirection;
startX = endX;
remainingLength -= characterLength;
if (remainingLength <= 0)
{
break;
}
}
return result;
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
{
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
}
return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
}
public TextLineImpl FinalizeLine()
{
_textLineMetrics = CreateLineMetrics();

39
src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs

@ -0,0 +1,39 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// The bounding rectangle of text run
/// </summary>
public sealed class TextRunBounds
{
/// <summary>
/// Constructing TextRunBounds
/// </summary>
internal TextRunBounds(Rect bounds, int firstCharacterIndex, int length, TextRun textRun)
{
Rectangle = bounds;
TextSourceCharacterIndex = firstCharacterIndex;
Length = length;
TextRun = textRun;
}
/// <summary>
/// First text source character index of text run
/// </summary>
public int TextSourceCharacterIndex { get; }
/// <summary>
/// character length of bounded text run
/// </summary>
public int Length { get; }
/// <summary>
/// Text run bounding rectangle
/// </summary>
public Rect Rectangle { get; }
/// <summary>
/// text run
/// </summary>
public TextRun TextRun { get; }
}
}

4
src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs

@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
{
Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize;
BidLevel = bidiLevel;
BidiLevel = bidiLevel;
Culture = culture;
IncrementalTabWidth = incrementalTabWidth;
}
@ -33,7 +33,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Get the bidi level of the text.
/// </summary>
public sbyte BidLevel { get; }
public sbyte BidiLevel { get; }
/// <summary>
/// Get the culture.

5
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -530,11 +530,6 @@ namespace Avalonia.Controls.Presenters
protected override Size MeasureOverride(Size availableSize)
{
if (string.IsNullOrEmpty(Text))
{
return new Size();
}
_constraint = availableSize;
_textLayout = null;

6
src/Avalonia.Controls/TextBlock.cs

@ -631,7 +631,11 @@ namespace Avalonia.Controls
return finalSize;
}
_constraint = new Size(finalSize.Width, double.PositiveInfinity);
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = new Size(finalSize.Deflate(padding).Width, double.PositiveInfinity);
_textLayout = null;

2
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -137,7 +137,7 @@ namespace Avalonia.Headless
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
}

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -16,7 +16,7 @@ namespace Avalonia.Skia
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())

2
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -16,7 +16,7 @@ namespace Avalonia.Direct2D1.Media
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())

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

@ -543,6 +543,98 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_Get_Distance_From_CharacterHit_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(10));
Assert.Equal(72.01171875, distance);
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(20));
Assert.Equal(144.0234375, distance);
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(30));
Assert.Equal(216.03515625, distance);
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(40));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, distance);
}
}
[Fact]
public void Should_Get_TextBounds_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var textBounds = textLine.GetTextBounds(0, 10);
Assert.Equal(1, textBounds.Count);
Assert.Equal(72.01171875, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, 20);
Assert.Equal(1, textBounds.Count);
Assert.Equal(144.0234375, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, 30);
Assert.Equal(1, textBounds.Count);
Assert.Equal(216.03515625, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, 40);
Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width);
}
}
private class MixedTextBufferTextSource : ITextSource
{
public TextRun? GetTextRun(int textSourceIndex)
{
switch (textSourceIndex)
{
case 0:
return new TextCharacters(new ReadOnlySlice<char>("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default));
case 10:
return new TextCharacters(new ReadOnlySlice<char>("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default));
case 20:
return new TextCharacters(new ReadOnlySlice<char>("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default));
case 30:
return new TextCharacters(new ReadOnlySlice<char>("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default));
default:
return null;
}
}
}
private class DrawableRunTextSource : ITextSource
{
const string Text = "_A_A";
@ -713,35 +805,95 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
[Fact]
public void Should_Get_TextBounds_BiDi()
public void Should_Get_TextBounds_BiDi_LeftToRight()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "0123".AsMemory();
var ltrOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
var rtlOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 1, CultureInfo.CurrentCulture);
var text = "אאא AAA";
var textSource = new SingleBufferTextSource(text, defaultProperties);
var textRuns = new List<TextRun>
{
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text), ltrOptions), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length, text.Length), ltrOptions), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2, text.Length), rtlOptions), defaultProperties),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
};
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
var textSource = new FixedRunsTextSource(textRuns);
var textBounds = textLine.GetTextBounds(0, 3);
var firstRun = textLine.TextRuns[0] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(3, 4);
var secondRun = textLine.TextRuns[1] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 4);
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);
textBounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(2, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}
[Fact]
public void Should_Get_TextBounds_BiDi_RightToLeft()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "אאא AAA";
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
var textBounds = textLine.GetTextBounds(0, 4);
var firstRun = textLine.TextRuns[1] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(4, 3);
var secondRun = textLine.TextRuns[0] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length));
Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 5);
Assert.Equal(2, textBounds.Count);
Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
Assert.Equal(textLine.Start + 7.201171875, textBounds[1].Rectangle.Right);
var textBounds = textLine.GetTextBounds(0, text.Length * 4);
textBounds = textLine.GetTextBounds(0, text.Length);
Assert.Equal(3, textBounds.Count);
Assert.Equal(2, textBounds.Count);
Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}

2
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -15,7 +15,7 @@ namespace Avalonia.UnitTests
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var culture = options.Culture;
using (var buffer = new Buffer())

2
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -11,7 +11,7 @@ namespace Avalonia.UnitTests
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);

Loading…
Cancel
Save