diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml
index f601f9f78f..1c74c07b1d 100644
--- a/samples/Sandbox/App.axaml
+++ b/samples/Sandbox/App.axaml
@@ -3,6 +3,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sandbox.App">
-
+
diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml
index 0c5a7a11e3..957616579a 100644
--- a/samples/Sandbox/MainWindow.axaml
+++ b/samples/Sandbox/MainWindow.axaml
@@ -1,5 +1,17 @@
-
+
+
+ أَبْجَدِيَّة عَرَبِيَّة
+
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index f3af240c58..8ab9591faf 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
- MaxLines = maxLines;
+ MaxLines = maxLines;
TextLines = CreateTextLines();
}
@@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum number of text lines.
public TextLayout(
ITextSource textSource,
- TextParagraphProperties paragraphProperties,
+ TextParagraphProperties paragraphProperties,
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
@@ -178,24 +178,18 @@ namespace Avalonia.Media.TextFormatting
return new Rect();
}
- if (textPosition < 0 || textPosition >= _textSourceLength)
+ if (textPosition < 0)
{
- var lastLine = TextLines[TextLines.Count - 1];
-
- var lineX = lastLine.Width;
-
- var lineY = Bounds.Bottom - lastLine.Height;
-
- return new Rect(lineX, lineY, 0, lastLine.Height);
+ textPosition = _textSourceLength;
}
var currentY = 0.0;
foreach (var textLine in TextLines)
{
- var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
+ var end = textLine.FirstTextSourceIndex + textLine.Length;
- if (end < textPosition)
+ if (end <= textPosition && end < _textSourceLength)
{
currentY += textLine.Height;
@@ -224,7 +218,7 @@ namespace Avalonia.Media.TextFormatting
}
var result = new List(TextLines.Count);
-
+
var currentY = 0d;
foreach (var textLine in TextLines)
@@ -239,7 +233,7 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length);
- if(textBounds.Count > 0)
+ if (textBounds.Count > 0)
{
foreach (var bounds in textBounds)
{
@@ -262,7 +256,7 @@ namespace Avalonia.Media.TextFormatting
}
}
- if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
+ if (textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
@@ -305,7 +299,7 @@ namespace Avalonia.Media.TextFormatting
return GetHitTestResult(currentLine, characterHit, point);
}
-
+
public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
{
if (charIndex < 0)
@@ -327,7 +321,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
- if (charIndex >= textLine.FirstTextSourceIndex &&
+ if (charIndex >= textLine.FirstTextSourceIndex &&
charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
@@ -398,7 +392,7 @@ namespace Avalonia.Media.TextFormatting
/// The current left.
/// The current width.
/// The current height.
- private static void UpdateBounds(TextLine textLine,ref double left, ref double width, ref double height)
+ private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
@@ -421,7 +415,7 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
- Bounds = new Rect(0,0,0, textLine.Height);
+ Bounds = new Rect(0, 0, 0, textLine.Height);
return new List { textLine };
}
@@ -439,9 +433,9 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
- if(textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+ if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
- if(previousLine != null && previousLine.NewLineLength > 0)
+ if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
@@ -454,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
}
_textSourceLength += textLine.Length;
-
+
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{
@@ -490,7 +484,7 @@ namespace Avalonia.Media.TextFormatting
}
//Make sure the TextLayout always contains at least on empty line
- if(textLines.Count == 0)
+ if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
@@ -501,7 +495,7 @@ namespace Avalonia.Media.TextFormatting
Bounds = new Rect(left, 0, width, height);
- if(_paragraphProperties.TextAlignment == TextAlignment.Justify)
+ if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
@@ -509,7 +503,7 @@ namespace Avalonia.Media.TextFormatting
{
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
- if(lineWhitespaceWidth > whitespaceWidth)
+ if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
@@ -517,7 +511,7 @@ namespace Avalonia.Media.TextFormatting
var justificationWidth = width - whitespaceWidth;
- if(justificationWidth > 0)
+ if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index f4a0324d90..67c8f0c88c 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -317,7 +317,7 @@ namespace Avalonia.Media.TextFormatting
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
- if (currentPosition + remainingLength < currentPosition + currentRun.Text.Length)
+ if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
{
characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
@@ -524,27 +524,30 @@ namespace Avalonia.Media.TextFormatting
characterLength = NewLineLength;
}
- var runwidth = endX - startX;
- var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun);
+ 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))
+ if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + runwidth);
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ {
+ currentRect = currentRect.WithWidth(currentWidth + runWidth);
- var textBounds = result[result.Count - 1];
+ var textBounds = result[result.Count - 1];
- textBounds.Rectangle = currentRect;
+ textBounds.Rectangle = currentRect;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ textBounds.TextRunBounds.Add(currentRunBounds);
+ }
+ else
+ {
+ currentRect = currentRunBounds.Rectangle;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ }
}
- currentWidth += runwidth;
+ currentWidth += runWidth;
currentPosition += characterLength;
if (currentPosition > characterIndex)
@@ -671,22 +674,25 @@ namespace Avalonia.Media.TextFormatting
var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
+ if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + runWidth);
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
+ {
+ currentRect = currentRect.WithWidth(currentWidth + runWidth);
- var textBounds = result[result.Count - 1];
+ var textBounds = result[result.Count - 1];
- textBounds.Rectangle = currentRect;
+ textBounds.Rectangle = currentRect;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ textBounds.TextRunBounds.Add(currentRunBounds);
+ }
+ else
+ {
+ currentRect = currentRunBounds.Rectangle;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
- }
+ result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ }
+ }
currentWidth += runWidth;
currentPosition += characterLength;
@@ -705,6 +711,8 @@ namespace Avalonia.Media.TextFormatting
}
}
+ result.Reverse();
+
return result;
}
diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs
index 2b84113497..6a40144137 100644
--- a/src/Avalonia.Controls/RichTextBlock.cs
+++ b/src/Avalonia.Controls/RichTextBlock.cs
@@ -41,9 +41,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty SelectionBrushProperty =
AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
- public static readonly StyledProperty SelectionForegroundBrushProperty =
- AvaloniaProperty.Register(nameof(SelectionForegroundBrush));
-
///
/// Defines the property.
///
@@ -68,7 +65,7 @@ namespace Avalonia.Controls
{
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
- AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty);
+ AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty);
}
public RichTextBlock()
@@ -89,15 +86,6 @@ namespace Avalonia.Controls
set => SetValue(SelectionBrushProperty, value);
}
- ///
- /// Gets or sets a value that defines the brush used for selected text.
- ///
- public IBrush? SelectionForegroundBrush
- {
- get => GetValue(SelectionForegroundBrushProperty);
- set => SetValue(SelectionForegroundBrushProperty, value);
- }
-
///
/// Gets or sets a character index for the beginning of the current selection.
///
@@ -198,7 +186,7 @@ namespace Avalonia.Controls
}
}
- public override void Render(DrawingContext context)
+ protected override void RenderTextLayout(DrawingContext context, Point origin)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@@ -213,13 +201,16 @@ namespace Avalonia.Controls
var rects = TextLayout.HitTestTextRange(start, length);
- foreach (var rect in rects)
+ using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
{
- context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
+ foreach (var rect in rects)
+ {
+ context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
+ }
}
}
- base.Render(context);
+ base.RenderTextLayout(context, origin);
}
///
@@ -280,8 +271,9 @@ namespace Avalonia.Controls
/// A object.
protected override TextLayout CreateTextLayout(string? text)
{
+ var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var defaultProperties = new GenericTextRunProperties(
- new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
+ typeface,
FontSize,
TextDecorations,
Foreground);
@@ -328,6 +320,8 @@ namespace Avalonia.Controls
protected override void OnKeyDown(KeyEventArgs e)
{
+ base.OnKeyDown(e);
+
var handled = false;
var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetRequiredService();
@@ -346,6 +340,8 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
+ base.OnPointerPressed(e);
+
if (!IsTextSelectionEnabled)
{
return;
@@ -356,7 +352,9 @@ namespace Avalonia.Controls
if (text != null && clickInfo.Properties.IsLeftButtonPressed)
{
- var point = e.GetPosition(this);
+ var padding = Padding;
+
+ var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
@@ -403,6 +401,8 @@ namespace Avalonia.Controls
protected override void OnPointerMoved(PointerEventArgs e)
{
+ base.OnPointerMoved(e);
+
if (!IsTextSelectionEnabled)
{
return;
@@ -411,11 +411,13 @@ namespace Avalonia.Controls
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
- var point = e.GetPosition(this);
+ var padding = Padding;
+
+ var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
point = new Point(
- MathUtilities.Clamp(point.X, 0, Math.Max(Bounds.Width - 1, 0)),
- MathUtilities.Clamp(point.Y, 0, Math.Max(Bounds.Height - 1, 0)));
+ MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)),
+ MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0)));
var hit = TextLayout.HitTestPoint(point);
@@ -425,6 +427,8 @@ namespace Avalonia.Controls
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
+ base.OnPointerReleased(e);
+
if (!IsTextSelectionEnabled)
{
return;
@@ -437,7 +441,9 @@ namespace Avalonia.Controls
if (e.InitialPressMouseButton == MouseButton.Right)
{
- var point = e.GetPosition(this);
+ var padding = Padding;
+
+ var point = e.GetPosition(this) - new Point(padding.Left, padding.Top);
var hit = TextLayout.HitTestPoint(point);
@@ -470,11 +476,6 @@ namespace Avalonia.Controls
InvalidateTextLayout();
break;
}
- case nameof(TextProperty):
- {
- InvalidateTextLayout();
- break;
- }
}
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 52261d1c76..baa722a8fe 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -509,7 +509,12 @@ namespace Avalonia.Controls
}
}
- TextLayout.Draw(context, new Point(padding.Left, top));
+ RenderTextLayout(context, new Point(padding.Left, top));
+ }
+
+ protected virtual void RenderTextLayout(DrawingContext context, Point origin)
+ {
+ TextLayout.Draw(context, origin);
}
protected virtual string? GetText()
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index 631d0881b0..1241141ccf 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -910,6 +910,51 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_Get_CharacterHit_From_Distance_RTL()
+ {
+ using (Start())
+ {
+ var text = "أَبْجَدِيَّة عَرَبِيَّة";
+
+ var layout = new TextLayout(
+ text,
+ Typeface.Default,
+ 12,
+ Brushes.Black);
+
+ var textLine = layout.TextLines[0];
+
+ var firstRun = textLine.TextRuns[0] as ShapedTextCharacters;
+
+ var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0];
+
+ var characterHit = textLine.GetCharacterHitFromDistance(0);
+
+ Assert.Equal(firstCluster, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(text.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+
+ var distance = textLine.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(0, distance);
+
+ distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
+
+ var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0];
+
+ Assert.Equal(firstAdvance, distance);
+
+ var rect = layout.HitTestTextPosition(22);
+
+ Assert.Equal(firstAdvance, rect.Left);
+
+ rect = layout.HitTestTextPosition(23);
+
+ Assert.Equal(0, rect.Left);
+ }
+ }
+
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 58cb84c4a4..d744ede87d 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -867,28 +867,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textBounds = textLine.GetTextBounds(0, 4);
- var firstRun = textLine.TextRuns[1] as ShapedTextCharacters;
+ var secondRun = textLine.TextRuns[1] as ShapedTextCharacters;
Assert.Equal(1, textBounds.Count);
- Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
+ Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(4, 3);
- var secondRun = textLine.TextRuns[0] as ShapedTextCharacters;
+ var firstRun = 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));
+ Assert.Equal(firstRun.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);
+ Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
+ Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
+ Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right);
+ Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left);
textBounds = textLine.GetTextBounds(0, text.Length);
@@ -896,7 +897,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
- }
+ }
private class FixedRunsTextSource : ITextSource
{