diff --git a/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
index a0b51671f07..93edf68348f 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs
@@ -10,20 +10,27 @@ public sealed class TextBounds
///
/// Constructing TextBounds object
///
- internal TextBounds(Rect bounds, FlowDirection flowDirection)
+ internal TextBounds(Rect bounds, FlowDirection flowDirection, IList runBounds)
{
Rectangle = bounds;
FlowDirection = flowDirection;
+ TextRunBounds = runBounds;
}
///
/// Bounds rectangle
///
- public Rect Rectangle { get; }
+ public Rect Rectangle { get; internal set; }
///
/// Text flow direction inside the boundary rectangle
///
public FlowDirection FlowDirection { get; }
+
+ ///
+ /// Get a list of run bounding rectangles
+ ///
+ /// Array of text run bounds
+ public IList TextRunBounds { get; }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 5d5d45db2d8..4f7c43a6d19 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -230,7 +230,7 @@ public IEnumerable HitTestTextRange(int start, int length)
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 @@ public IEnumerable HitTestTextRange(int start, int 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;
-
- 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)
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index 73ec055bbec..8b5e2cc2ced 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -184,6 +184,10 @@ public override CharacterHit GetCharacterHitFromDistance(double distance)
{
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 @@ public override CharacterHit GetCharacterHitFromDistance(double distance)
///
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 @@ public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
}
//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 @@ public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
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 @@ public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
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 @@ public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
//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 @@ public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characte
return GetPreviousCaretCharacterHit(characterHit);
}
- public override IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
+ private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
{
- if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
- {
- return Array.Empty();
- }
+ var characterIndex = firstTextSourceIndex + textLength;
var result = new List(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 { 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 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(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 { 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 GetTextBounds(int firstTextSourceIndex, int textLength)
+ {
+ if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
+ {
+ return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
+ }
+
+ return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
+ }
+
public TextLineImpl FinalizeLine()
{
_textLineMetrics = CreateLineMetrics();
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
new file mode 100644
index 00000000000..91150160ed2
--- /dev/null
+++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
@@ -0,0 +1,39 @@
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// The bounding rectangle of text run
+ ///
+ public sealed class TextRunBounds
+ {
+ ///
+ /// Constructing TextRunBounds
+ ///
+ internal TextRunBounds(Rect bounds, int firstCharacterIndex, int length, TextRun textRun)
+ {
+ Rectangle = bounds;
+ TextSourceCharacterIndex = firstCharacterIndex;
+ Length = length;
+ TextRun = textRun;
+ }
+
+ ///
+ /// First text source character index of text run
+ ///
+ public int TextSourceCharacterIndex { get; }
+
+ ///
+ /// character length of bounded text run
+ ///
+ public int Length { get; }
+
+ ///
+ /// Text run bounding rectangle
+ ///
+ public Rect Rectangle { get; }
+
+ ///
+ /// text run
+ ///
+ public TextRun TextRun { get; }
+ }
+}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
index a7fe92dc9a5..4e75bb921eb 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
+++ b/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
///
/// Get the bidi level of the text.
///
- public sbyte BidLevel { get; }
+ public sbyte BidiLevel { get; }
///
/// Get the culture.
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index ea9ae7bb0f9..3523cd52140 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -530,11 +530,6 @@ protected virtual void InvalidateTextLayout()
protected override Size MeasureOverride(Size availableSize)
{
- if (string.IsNullOrEmpty(Text))
- {
- return new Size();
- }
-
_constraint = availableSize;
_textLayout = null;
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index bbe6aeb7eea..1a69d1218c7 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -631,7 +631,11 @@ protected override Size ArrangeOverride(Size 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;
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index 083b16c1074..22ff8e8f97e 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -137,7 +137,7 @@ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions option
{
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);
}
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index 908b0ffa477..777e9076179 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -16,7 +16,7 @@ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions option
{
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())
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index f4e4b00147a..6e32d329134 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -16,7 +16,7 @@ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions option
{
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())
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index a47638d2ec0..a974e063857 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -543,6 +543,98 @@ public void Should_Get_Distance_From_CharacterHit_Drawable_Runs()
}
}
+ [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("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+ case 10:
+ return new TextCharacters(new ReadOnlySlice("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+ case 20:
+ return new TextCharacters(new ReadOnlySlice("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+ case 30:
+ return new TextCharacters(new ReadOnlySlice("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+ default:
+ return null;
+ }
+ }
+ }
+
private class DrawableRunTextSource : ITextSource
{
const string Text = "_A_A";
@@ -713,35 +805,95 @@ public void Should_Get_TextBounds_Mixed()
}
[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
- {
- new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), ltrOptions), defaultProperties),
- new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length, text.Length), ltrOptions), defaultProperties),
- new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2, text.Length), rtlOptions), defaultProperties),
- new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(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));
}
}
diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
index 5f8854b3abc..4bc30484e97 100644
--- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
@@ -15,7 +15,7 @@ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions option
{
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())
diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
index c4b1e6c1545..7c34bd192e6 100644
--- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
+++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs
@@ -11,7 +11,7 @@ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions option
{
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);