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> /// <summary>
/// Constructing TextBounds object /// Constructing TextBounds object
/// </summary> /// </summary>
internal TextBounds(Rect bounds, FlowDirection flowDirection) internal TextBounds(Rect bounds, FlowDirection flowDirection, IList<TextRunBounds> runBounds)
{ {
Rectangle = bounds; Rectangle = bounds;
FlowDirection = flowDirection; FlowDirection = flowDirection;
TextRunBounds = runBounds;
} }
/// <summary> /// <summary>
/// Bounds rectangle /// Bounds rectangle
/// </summary> /// </summary>
public Rect Rectangle { get; } public Rect Rectangle { get; internal set; }
/// <summary> /// <summary>
/// Text flow direction inside the boundary rectangle /// Text flow direction inside the boundary rectangle
/// </summary> /// </summary>
public FlowDirection FlowDirection { get; } 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) foreach (var textLine in TextLines)
{ {
//Current line isn't covered. //Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length <= start) if (textLine.FirstTextSourceIndex + textLine.Length < start)
{ {
currentY += textLine.Height; currentY += textLine.Height;
@ -239,18 +239,27 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length); 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; foreach (var bounds in textBounds)
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); 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) 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 _); characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
break; break;
} }
default: default:
@ -215,9 +219,11 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) 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 currentDistance = Start;
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
GlyphRun? lastRun = null; GlyphRun? lastRun = null;
@ -242,8 +248,10 @@ namespace Avalonia.Media.TextFormatting
} }
//Look for a hit in within the current run //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); var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
return currentDistance + distance; return currentDistance + distance;
@ -254,28 +262,27 @@ namespace Avalonia.Media.TextFormatting
{ {
if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{ {
if (characterIndex <= textRun.Text.Start) if (characterIndex <= currentPosition)
{ {
return currentDistance; return currentDistance;
} }
} }
else else
{ {
if (characterIndex == textRun.Text.Start) if (characterIndex == currentPosition)
{ {
return currentDistance; return currentDistance;
} }
} }
if (characterIndex == textRun.Text.Start + textRun.Text.Length && if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit)
characterHit.TrailingLength > 0)
{ {
return currentDistance + currentRun.Size.Width; return currentDistance + currentRun.Size.Width;
} }
} }
else else
{ {
if (characterIndex == textRun.Text.Start) if (characterIndex == currentPosition)
{ {
return currentDistance + currentRun.Size.Width; return currentDistance + currentRun.Size.Width;
} }
@ -286,20 +293,24 @@ namespace Avalonia.Media.TextFormatting
if (nextRun != null) if (nextRun != null)
{ {
if (characterHit.FirstCharacterIndex == textRun.Text.End && if (nextRun.ShapedBuffer.IsLeftToRight)
nextRun.ShapedBuffer.IsLeftToRight)
{ {
return currentDistance; if (characterIndex == currentPosition + textRun.Text.Length)
{
return currentDistance;
}
} }
else
if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
{ {
return currentDistance; if (currentPosition + nextRun.Text.Length == characterIndex)
{
return currentDistance;
}
} }
} }
else else
{ {
if (characterIndex > textRun.Text.End) if (characterIndex > currentPosition + textRun.Text.Length)
{ {
return currentDistance; return currentDistance;
} }
@ -329,6 +340,12 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width //No hit hit found so we add the full width
currentDistance += textRun.Size.Width; currentDistance += textRun.Size.Width;
currentPosition += textRun.TextSourceLength; currentPosition += textRun.TextSourceLength;
remainingLength -= textRun.TextSourceLength;
if (remainingLength <= 0)
{
break;
}
} }
return currentDistance; return currentDistance;
@ -394,210 +411,299 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit); return GetPreviousCaretCharacterHit(characterHit);
} }
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength) private IReadOnlyList<TextBounds> GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
{ {
if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex) var characterIndex = firstTextSourceIndex + textLength;
{
return Array.Empty<TextBounds>();
}
var result = new List<TextBounds>(TextRuns.Count); var result = new List<TextBounds>(TextRuns.Count);
var lastDirection = _flowDirection; var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection; var currentDirection = lastDirection;
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var currentRect = Rect.Empty; var remainingLength = textLength;
var startX = Start; 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++) for (var index = 0; index < TextRuns.Count; index++)
{ {
var currentRun = TextRuns[index] as DrawableTextRun; if (TextRuns[index] is not DrawableTextRun currentRun)
if (currentRun is null)
{ {
continue; continue;
} }
TextRun? nextRun = null; if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
if (index + 1 < TextRuns.Count)
{ {
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) var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
{
case ShapedTextCharacters when currentRun is ShapedTextCharacters:
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
{
goto skip;
}
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) currentPosition += offset;
{
goto skip;
}
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) var startIndex = currentRun.Text.Start + offset;
{
goto skip;
}
if (currentRun.Text.End < firstTextSourceCharacterIndex) var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
{ currentShapedRun.ShapedBuffer.IsLeftToRight ?
goto skip; new CharacterHit(startIndex + remainingLength) :
} new CharacterHit(startIndex));
goto noop; endX += endOffset;
}
default: var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
{ currentShapedRun.ShapedBuffer.IsLeftToRight ?
goto noop; 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; startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
} }
continue; if (currentPosition + currentRun.TextSourceLength <= characterIndex)
noop:
{ {
endX += currentRun.Size.Width;
characterLength = currentRun.TextSourceLength;
} }
} }
var endX = startX; if (endX < startX)
var endOffset = 0d; {
(endX, startX) = (startX, endX);
}
switch (currentRun) //Lines that only contain a linebreak need to be covered here
if(characterLength == 0)
{ {
case ShapedTextCharacters shapedRun: characterLength = NewLineLength;
{ }
endOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
shapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex + textLength) :
new CharacterHit(firstTextSourceCharacterIndex));
endX += endOffset; var runwidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun);
var startOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit( if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
shapedRun.ShapedBuffer.IsLeftToRight ? {
new CharacterHit(firstTextSourceCharacterIndex) : currentRect = currentRect.WithWidth(currentWidth + runwidth);
new CharacterHit(firstTextSourceCharacterIndex + textLength));
startX += startOffset; var textBounds = result[result.Count - 1];
var characterHit = shapedRun.GlyphRun.IsLeftToRight ? textBounds.Rectangle = currentRect;
shapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
shapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
currentDirection = shapedRun.ShapedBuffer.IsLeftToRight ? result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
FlowDirection.LeftToRight : }
FlowDirection.RightToLeft;
if (nextRun is ShapedTextCharacters nextShaped) currentWidth += runwidth;
{ currentPosition += characterLength;
if (shapedRun.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight)
{
endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit(
nextShaped.ShapedBuffer.IsLeftToRight ?
new CharacterHit(firstTextSourceCharacterIndex + textLength) :
new CharacterHit(firstTextSourceCharacterIndex));
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) return result;
{ }
characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; private IReadOnlyList<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
} {
} var characterIndex = firstTextSourceIndex + textLength;
}
break; var result = new List<TextBounds>(TextRuns.Count);
} var lastDirection = FlowDirection.LeftToRight;
default: var currentDirection = lastDirection;
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
{
endX += currentRun.Size.Width;
}
if (currentPosition < firstTextSourceCharacterIndex) var currentPosition = FirstTextSourceIndex;
{ var remainingLength = textLength;
startX += currentRun.Size.Width;
}
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)) var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
{
currentRect = currentRect.WithWidth(currentRect.Width + width); 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 (currentDirection == FlowDirection.LeftToRight)
{ {
if (currentPosition > firstTextSourceCharacterIndex + textLength) if (currentPosition > characterIndex)
{ {
break; break;
} }
} }
else else
{ {
if (currentPosition <= firstTextSourceCharacterIndex) if (currentPosition <= firstTextSourceIndex)
{ {
break; break;
} }
endX += currentRun.Size.Width - endOffset;
} }
lastDirection = currentDirection; lastDirection = currentDirection;
startX = endX; remainingLength -= characterLength;
if (remainingLength <= 0)
{
break;
}
} }
return result; 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() public TextLineImpl FinalizeLine()
{ {
_textLineMetrics = CreateLineMetrics(); _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; Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize; FontRenderingEmSize = fontRenderingEmSize;
BidLevel = bidiLevel; BidiLevel = bidiLevel;
Culture = culture; Culture = culture;
IncrementalTabWidth = incrementalTabWidth; IncrementalTabWidth = incrementalTabWidth;
} }
@ -33,7 +33,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Get the bidi level of the text. /// Get the bidi level of the text.
/// </summary> /// </summary>
public sbyte BidLevel { get; } public sbyte BidiLevel { get; }
/// <summary> /// <summary>
/// Get the culture. /// Get the culture.

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

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

6
src/Avalonia.Controls/TextBlock.cs

@ -631,7 +631,11 @@ namespace Avalonia.Controls
return finalSize; 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; _textLayout = null;

2
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -137,7 +137,7 @@ namespace Avalonia.Headless
{ {
var typeface = options.Typeface; var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel; var bidiLevel = options.BidiLevel;
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, 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 typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel; var bidiLevel = options.BidiLevel;
var culture = options.Culture; var culture = options.Culture;
using (var buffer = new Buffer()) 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 typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel; var bidiLevel = options.BidiLevel;
var culture = options.Culture; var culture = options.Culture;
using (var buffer = new Buffer()) 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 private class DrawableRunTextSource : ITextSource
{ {
const string Text = "_A_A"; const string Text = "_A_A";
@ -713,35 +805,95 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
[Fact] [Fact]
public void Should_Get_TextBounds_BiDi() public void Should_Get_TextBounds_BiDi_LeftToRight()
{ {
using (Start()) using (Start())
{ {
var defaultProperties = new GenericTextRunProperties(Typeface.Default); var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "0123".AsMemory(); var text = "אאא AAA";
var ltrOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); var textSource = new SingleBufferTextSource(text, defaultProperties);
var rtlOptions = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 1, CultureInfo.CurrentCulture);
var textRuns = new List<TextRun> var formatter = new TextFormatterImpl();
{
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 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 formatter = new TextFormatterImpl();
var textLine = var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity, formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(defaultProperties)); 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)); 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 typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidLevel; var bidiLevel = options.BidiLevel;
var culture = options.Culture; var culture = options.Culture;
using (var buffer = new Buffer()) using (var buffer = new Buffer())

2
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

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

Loading…
Cancel
Save