Browse Source

Merge branch 'master' into mobile-control-host

pull/8110/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
d7991c2afd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      readme.md
  2. 11
      src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
  3. 29
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  4. 376
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  5. 39
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  6. 4
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  7. 5
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  8. 6
      src/Avalonia.Controls/TextBlock.cs
  9. 2
      src/Avalonia.Controls/Viewbox.cs
  10. 2
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  11. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  12. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  13. 26
      tests/Avalonia.Controls.UnitTests/ViewboxTests.cs
  14. 184
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  15. 2
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  16. 2
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

15
readme.md

@ -70,11 +70,15 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou
Avalonia is licenced under the [MIT licence](licence.md).
## Support Avalonia
## Donate
**BTC**: bc1q05wx78qemgy9x6ytl5ljk2xrt00yqargyjm8gx
Donating to the project is a fantastic way to thank our valued contributors for their hard work. Your donations are shared among our community and awarded for significant contributions.
If you need support see Commercial Support section below.
Donate with BTC or use [Open Collective](https://opencollective.com/avalonia).
This will be shared with the community and awarded for significant contributions.
**BTC**: bc1q05wx78qemgy9x6ytl5ljk2xrt00yqargyjm8gx
### Backers
@ -98,6 +102,11 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
<a href="https://baseheadinc.com/" target="_blank"><img height="50" src="https://baseheadinc.com/wp-content/uploads/2020/09/BH-Logo-for-Site-Header-New.png"></a>
## Commercial Support
We have a range of [support plans available](https://avaloniaui.net/support.html) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process.
*Please note that donations are not considered payment for commercial support agreements. Please contact us to discuss your needs first. [team@avaloniaui.net](mailto://team@avaloniaui.net)*
## .NET Foundation
This project is supported by the [.NET Foundation](https://dotnetfoundation.org).

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.Controls/Viewbox.cs

@ -168,6 +168,8 @@ namespace Avalonia.Controls
if (_child is not null)
VisualChildren.Add(_child);
InvalidateMeasure();
}
}
}

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())

26
tests/Avalonia.Controls.UnitTests/ViewboxTests.cs

@ -181,6 +181,32 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child.GetLogicalParent());
}
[Fact]
public void Changing_Child_Should_Invalidate_Layout()
{
var target = new Viewbox();
target.Child = new Canvas
{
Width = 100,
Height = 100,
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(100, 100), target.DesiredSize);
target.Child = new Canvas
{
Width = 200,
Height = 200,
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(200, 200), target.DesiredSize);
}
private bool TryGetScale(Viewbox viewbox, out Vector scale)
{
if (viewbox.InternalTransform is null)

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