From cc507ba359703bfe040cda1595b0ac6b28d8d93c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 6 Jun 2023 16:58:13 +0200 Subject: [PATCH] Handle extra lines during hit testing --- src/Avalonia.Base/Media/GlyphRun.cs | 13 ++++++----- .../Media/TextFormatting/TextFormatterImpl.cs | 2 ++ .../Media/TextFormatting/TextLayout.cs | 22 ++++++++++--------- .../Media/TextFormatting/TextLineImpl.cs | 18 +++++++-------- .../Media/TextFormatting/TextLayoutTests.cs | 15 +++++++++++++ 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 350d8817f1..0f70386424 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -643,12 +643,13 @@ namespace Avalonia.Media lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } + var isReversed = firstCluster > lastCluster; + if (!IsLeftToRight) { (lastCluster, firstCluster) = (firstCluster, lastCluster); } - var isReversed = firstCluster > lastCluster; var height = GlyphTypeface.Metrics.LineSpacing * Scale; var widthIncludingTrailingWhitespace = 0d; @@ -766,15 +767,13 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - var characterIndex = 0; + var characterIndex = charactersSpan.Length - 1; for (var i = 0; i < _glyphInfos.Count; i++) { var currentCluster = _glyphInfos[i].GlyphCluster; var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - characterIndex += characterLength; - if (!codepoint.IsWhiteSpace) { break; @@ -784,9 +783,9 @@ namespace Avalonia.Media var j = i; - while (j - 1 >= 0) + while (j + 1 < _glyphInfos.Count) { - var nextCluster = _glyphInfos[--j].GlyphCluster; + var nextCluster = _glyphInfos[++j].GlyphCluster; if (currentCluster == nextCluster) { @@ -798,6 +797,8 @@ namespace Avalonia.Media break; } + characterIndex -= clusterLength; + if (codepoint.IsBreakChar) { newLineLength += clusterLength; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index a609800fb8..d2198a2cbf 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -684,7 +684,9 @@ namespace Avalonia.Media.TextFormatting var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + line.FinalizeLine(); + return line; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4ccb3f6a37..b6b6d11a49 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting /// /// Gets the text spacing. /// - public double LetterSpacing => _paragraphProperties.LetterSpacing; + public double LetterSpacing => _paragraphProperties.LetterSpacing; /// /// Gets the text lines. @@ -271,11 +271,13 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in _textLines) + for (var i = 0; i < _textLines.Length; i++) { + var textLine = _textLines[i]; + var end = textLine.FirstTextSourceIndex + textLine.Length; - if (end <= textPosition && end < _textSourceLength) + if (end <= textPosition && i + 1 < _textLines.Length) { currentY += textLine.Height; @@ -511,7 +513,7 @@ namespace Avalonia.Media.TextFormatting { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); - UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, + UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); return new TextLine[] { textLine }; @@ -638,13 +640,13 @@ namespace Avalonia.Media.TextFormatting } private void UpdateMetrics( - TextLine currentLine, - ref double lineStartOfLongestLine, - ref Point origin, - ref bool first, + TextLine currentLine, + ref double lineStartOfLongestLine, + ref Point origin, + ref bool first, ref double accBlackBoxLeft, - ref double accBlackBoxTop, - ref double accBlackBoxRight, + ref double accBlackBoxTop, + ref double accBlackBoxRight, ref double accBlackBoxBottom) { var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 588a30de85..ca31d9a6d0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -640,14 +640,14 @@ namespace Avalonia.Media.TextFormatting bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds) { - if(currentBounds.FlowDirection != lastBounds.FlowDirection) + if (currentBounds.FlowDirection != lastBounds.FlowDirection) { return false; } - if(currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) + if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right) { - foreach(var runBounds in currentBounds.TextRunBounds) + foreach (var runBounds in currentBounds.TextRunBounds) { lastBounds.TextRunBounds.Add(runBounds); } @@ -657,7 +657,7 @@ namespace Avalonia.Media.TextFormatting return true; } - if(currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) + if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left) { for (int i = 0; i < currentBounds.TextRunBounds.Count; i++) { @@ -730,7 +730,7 @@ namespace Avalonia.Media.TextFormatting if (coveredLength > 0) { - if(lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) + if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) { currentBounds = lastBounds; @@ -739,7 +739,7 @@ namespace Avalonia.Media.TextFormatting else { result.Add(currentBounds); - } + } lastBounds = currentBounds; @@ -1002,14 +1002,14 @@ namespace Avalonia.Media.TextFormatting public void FinalizeLine() { + _indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex); + _textLineMetrics = CreateLineMetrics(); if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) { _textLineBreak = new TextLineBreak(textEndOfLine); - } - - _indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex); + } } /// diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 71aeb4397e..ac444e37df 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1112,6 +1112,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_HitTestTextPosition_EndOfLine_RTL() + { + var text = "גש\r\n"; + + using (Start()) + { + var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: FlowDirection.RightToLeft); + + var rect = textLayout.HitTestTextPosition(text.Length); + + Assert.Equal(14.0625, rect.Top); + } + } + private static IDisposable Start() {