From 7cbeb7ad3f4aac3412efd662d5dac4f6f984cdbb Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 8 Jul 2022 18:56:29 +0200 Subject: [PATCH 01/16] Rework TextLineImpl hit testing --- .../ControlCatalog/Pages/TextBlockPage.xaml | 2 +- samples/Sandbox/MainWindow.axaml | 1 + samples/Sandbox/Sandbox.csproj | 1 + src/Avalonia.Base/Media/GlyphRun.cs | 109 ++++- .../Media/TextFormatting/TextLineImpl.cs | 371 +++++++++--------- .../Media/TextFormatting/TextLayoutTests.cs | 84 +++- 6 files changed, 366 insertions(+), 202 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index cb49ba96c6..32914428ed 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -118,7 +118,7 @@ - + This is a TextBlock with several diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..0c5a7a11e3 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,5 @@ + diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index eab654acb6..20c7f29201 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index ac87d521a5..703b56b0e8 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -614,17 +614,29 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { + var firstCluster = 0; + var lastCluster = Characters.Length - 1; + + if (!IsLeftToRight) + { + var cluster = firstCluster; + firstCluster = lastCluster; + lastCluster = cluster; + } + if (GlyphClusters != null && GlyphClusters.Count > 0) { - var firstCluster = GlyphClusters[0]; + firstCluster = GlyphClusters[0]; + lastCluster = GlyphClusters[GlyphClusters.Count - 1]; _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); } + var isReversed = firstCluster > lastCluster; var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var widthIncludingTrailingWhitespace = 0d; - var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount); + var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); for (var index = 0; index < GlyphIndices.Count; index++) { @@ -635,16 +647,16 @@ namespace Avalonia.Media var width = widthIncludingTrailingWhitespace; - if (IsLeftToRight) + if (isReversed) { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = 0; index < glyphCount; index++) { width -= GetGlyphAdvance(index, out _); - } + } } else { - for (var index = 0; index < glyphCount; index++) + for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) { width -= GetGlyphAdvance(index, out _); } @@ -654,16 +666,15 @@ namespace Avalonia.Media height); } - private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount) - { - glyphCount = 0; - newLineLength = 0; - - if (Characters.IsEmpty) + private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount) + { + if (isReversed) { - return 0; + return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } + glyphCount = 0; + newLineLength = 0; var trailingWhitespaceLength = 0; if (GlyphClusters == null) @@ -732,6 +743,78 @@ namespace Avalonia.Media return trailingWhitespaceLength; } + private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + { + glyphCount = 0; + newLineLength = 0; + var trailingWhitespaceLength = 0; + + if (GlyphClusters == null) + { + for (var i = 0; i < Characters.Length;) + { + var codepoint = Codepoint.ReadAt(_characters, i, out var count); + + if (!codepoint.IsWhiteSpace) + { + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength++; + } + + trailingWhitespaceLength++; + + i += count; + glyphCount++; + } + } + else + { + for (var i = 0; i < GlyphClusters.Count; i++) + { + var currentCluster = GlyphClusters[i]; + var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); + + if (!codepoint.IsWhiteSpace) + { + break; + } + + var clusterLength = 1; + + while (i - 1 >= 0) + { + var nextCluster = GlyphClusters[i - 1]; + + if (currentCluster == nextCluster) + { + clusterLength++; + i--; + + continue; + } + + break; + } + + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; + } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; + } + } + + return trailingWhitespaceLength; + } + private void Set(ref T field, T value) { _glyphRunImpl?.Dispose(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7c686358e2..f4a0324d90 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -166,58 +166,74 @@ namespace Avalonia.Media.TextFormatting if (distance <= 0) { - // hit happens before the line, return the first position var firstRun = _textRuns[0]; - if (firstRun is ShapedTextCharacters shapedTextCharacters) - { - return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _); - } + return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + } - return _resolvedFlowDirection == FlowDirection.LeftToRight ? - new CharacterHit(FirstTextSourceIndex) : - new CharacterHit(FirstTextSourceIndex + Length); + if (distance > WidthIncludingTrailingWhitespace) + { + var lastRun = _textRuns[_textRuns.Count - 1]; + + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width); } // process hit that happens within the line var characterHit = new CharacterHit(); var currentPosition = FirstTextSourceIndex; + var currentDistance = 0.0; foreach (var currentRun in _textRuns) { - switch (currentRun) + if (currentDistance + currentRun.Size.Width < distance) { - case ShapedTextCharacters shapedRun: - { - characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + currentDistance += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; - var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + continue; + } - characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); + characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - break; - } - default: + break; + } + + return characterHit; + } + + private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance) + { + CharacterHit characterHit; + + switch (run) + { + case ShapedTextCharacters shapedRun: + { + characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + + var offset = Math.Max(0, currentPosition - shapedRun.Text.Start); + + if (!shapedRun.GlyphRun.IsLeftToRight) { - if (distance < currentRun.Size.Width / 2) - { - characterHit = new CharacterHit(currentPosition); - } - else - { - characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength); - } - break; + offset = Math.Max(0, offset - shapedRun.Text.End); } - } - if (distance <= currentRun.Size.Width) - { - break; - } + characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); - distance -= currentRun.Size.Width; - currentPosition += currentRun.TextSourceLength; + break; + } + default: + { + if (distance < run.Size.Width / 2) + { + characterHit = new CharacterHit(currentPosition); + } + else + { + characterHit = new CharacterHit(currentPosition, run.TextSourceLength); + } + break; + } } return characterHit; @@ -226,136 +242,122 @@ namespace Avalonia.Media.TextFormatting /// public override double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var isTrailingHit = characterHit.TrailingLength > 0; + var flowDirection = _paragraphProperties.FlowDirection; var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var currentDistance = Start; var currentPosition = FirstTextSourceIndex; var remainingLength = characterIndex - FirstTextSourceIndex; - GlyphRun? lastRun = null; + var currentDistance = Start; - for (var index = 0; index < _textRuns.Count; index++) + if (flowDirection == FlowDirection.LeftToRight) + { + for (var index = 0; index < _textRuns.Count; index++) + { + var currentRun = _textRuns[index]; + + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, + flowDirection, out var distance, out _)) + { + return currentDistance + distance; + } + + //No hit hit found so we add the full width + currentDistance += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; + remainingLength -= currentRun.TextSourceLength; + } + } + else { - var textRun = _textRuns[index]; + currentDistance += WidthIncludingTrailingWhitespace; - switch (textRun) + for (var index = _textRuns.Count - 1; index >= 0; index--) { - case ShapedTextCharacters shapedTextCharacters: + var currentRun = _textRuns[index]; + + if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, + flowDirection, out var distance, out var currentGlyphRun)) + { + if (currentGlyphRun != null) { - var currentRun = shapedTextCharacters.GlyphRun; + distance = currentGlyphRun.Size.Width - distance; + } - if (lastRun != null) - { - if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight && - currentRun.Characters.Start == characterHit.FirstCharacterIndex && - characterHit.TrailingLength == 0) - { - return currentDistance; - } - } + return currentDistance - distance; + } - //Look for a hit in within the current run - if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length) - { - characterHit = new CharacterHit(textRun.Text.Start + remainingLength); + //No hit hit found so we add the full width + currentDistance -= currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; + remainingLength -= currentRun.TextSourceLength; + } + } - var distance = currentRun.GetDistanceFromCharacterHit(characterHit); + return currentDistance; + } - return currentDistance + distance; - } + private static bool TryGetDistanceFromCharacterHit( + DrawableTextRun currentRun, + CharacterHit characterHit, + int currentPosition, + int remainingLength, + FlowDirection flowDirection, + out double distance, + out GlyphRun? currentGlyphRun) + { + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + var isTrailingHit = characterHit.TrailingLength > 0; - //Look at the left and right edge of the current run - if (currentRun.IsLeftToRight) - { - if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) - { - if (characterIndex <= currentPosition) - { - return currentDistance; - } - } - else - { - if (characterIndex == currentPosition) - { - return currentDistance; - } - } + distance = 0; + currentGlyphRun = null; - if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit) - { - return currentDistance + currentRun.Size.Width; - } - } - else - { - if (characterIndex == currentPosition) - { - return currentDistance + currentRun.Size.Width; - } - - var nextRun = index + 1 < _textRuns.Count ? - _textRuns[index + 1] as ShapedTextCharacters : - null; + switch (currentRun) + { + case ShapedTextCharacters shapedTextCharacters: + { + currentGlyphRun = shapedTextCharacters.GlyphRun; - if (nextRun != null) - { - if (nextRun.ShapedBuffer.IsLeftToRight) - { - if (characterIndex == currentPosition + textRun.Text.Length) - { - return currentDistance; - } - } - else - { - if (currentPosition + nextRun.Text.Length == characterIndex) - { - return currentDistance; - } - } - } - else - { - if (characterIndex > currentPosition + textRun.Text.Length) - { - return currentDistance; - } - } - } + if (currentPosition + remainingLength < currentPosition + currentRun.Text.Length) + { + characterHit = new CharacterHit(currentRun.Text.Start + remainingLength); - lastRun = currentRun; + distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); - break; + return true; } - default: + + if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit) { - if (characterIndex == currentPosition) + if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - return currentDistance; + distance = currentGlyphRun.Size.Width; } - if (characterIndex == currentPosition + textRun.TextSourceLength) - { - return currentDistance + textRun.Size.Width; - } + return true; + } - break; + break; + } + default: + { + if (characterIndex == currentPosition) + { + return true; } - } - //No hit hit found so we add the full width - currentDistance += textRun.Size.Width; - currentPosition += textRun.TextSourceLength; - remainingLength -= textRun.TextSourceLength; + if (characterIndex == currentPosition + currentRun.TextSourceLength) + { + distance = currentRun.Size.Width; - if (remainingLength <= 0) - { - break; - } + return true; + + } + + break; + } } - return currentDistance; + return false; } /// @@ -460,20 +462,33 @@ namespace Avalonia.Media.TextFormatting var startIndex = currentRun.Text.Start + offset; - var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex + remainingLength) : - new CharacterHit(startIndex)); + double startOffset; + double endOffset; - endX += endOffset; + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex) : - new CharacterHit(startIndex + remainingLength)); + if (currentPosition < startIndex) + { + startOffset = endOffset; + } + else + { + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } + } startX += startOffset; + endX += endOffset; + var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); @@ -504,7 +519,7 @@ namespace Avalonia.Media.TextFormatting } //Lines that only contain a linebreak need to be covered here - if(characterLength == 0) + if (characterLength == 0) { characterLength = NewLineLength; } @@ -532,19 +547,9 @@ namespace Avalonia.Media.TextFormatting currentWidth += runwidth; currentPosition += characterLength; - if (currentDirection == FlowDirection.LeftToRight) + if (currentPosition > characterIndex) { - if (currentPosition > characterIndex) - { - break; - } - } - else - { - if (currentPosition <= firstTextSourceIndex) - { - break; - } + break; } startX = endX; @@ -571,7 +576,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start + WidthIncludingTrailingWhitespace; + var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; var currentRect = Rect.Empty; @@ -582,7 +587,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex) { startX -= currentRun.Size.Width; @@ -601,20 +606,31 @@ namespace Avalonia.Media.TextFormatting currentPosition += offset; var startIndex = currentRun.Text.Start + offset; + double startOffset; + double endOffset; - var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex + remainingLength) : - new CharacterHit(startIndex)); + if (currentShapedRun.ShapedBuffer.IsLeftToRight) + { + if (currentPosition < startIndex) + { + startOffset = endOffset = 0; + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - endX += endOffset - currentShapedRun.Size.Width; + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + } + } + else + { + endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit( - currentShapedRun.ShapedBuffer.IsLeftToRight ? - new CharacterHit(startIndex) : - new CharacterHit(startIndex + remainingLength)); + startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + } - startX += startOffset - currentShapedRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); @@ -652,9 +668,10 @@ namespace Avalonia.Media.TextFormatting } 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)) + 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)) { currentRect = currentRect.WithWidth(currentWidth + runWidth); @@ -674,19 +691,9 @@ namespace Avalonia.Media.TextFormatting currentWidth += runWidth; currentPosition += characterLength; - if (currentDirection == FlowDirection.LeftToRight) + if (currentPosition > characterIndex) { - if (currentPosition > characterIndex) - { - break; - } - } - else - { - if (currentPosition <= firstTextSourceIndex) - { - break; - } + break; } lastDirection = currentDirection; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 7e1103d624..631d0881b0 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -154,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { j += inner.Current.Text.Length; - if(j + i > text.Length) + if (j + i > text.Length) { break; } @@ -738,7 +738,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1)); - + var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1)); var rects = layout.HitTestTextRange(0, 7).ToArray(); @@ -746,7 +746,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, rects.Length); var expected = rects[0]; - + Assert.Equal(expected.Left, start); Assert.Equal(expected.Right, end); } @@ -818,11 +818,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); Assert.Equal(expected, actual); - } + } } } } - + [Fact] public void Should_Layout_Empty_String() { @@ -833,11 +833,83 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Typeface.Default, 12, Brushes.Black); - + Assert.True(layout.Bounds.Height > 0); } } + [Fact] + public void Should_HitTestPoint_RightToLeft() + { + using (Start()) + { + var text = "אאא AAA"; + + var layout = new TextLayout( + text, + Typeface.Default, + 12, + Brushes.Black, + flowDirection: FlowDirection.RightToLeft); + + var firstRun = layout.TextLines[0].TextRuns[0] as ShapedTextCharacters; + + var hit = layout.HitTestPoint(new Point()); + + Assert.Equal(4, hit.TextPosition); + + var currentX = 0.0; + + for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + { + var cluster = firstRun.GlyphRun.GlyphClusters[i]; + var advance = firstRun.GlyphRun.GlyphAdvances[i]; + + hit = layout.HitTestPoint(new Point(currentX, 0)); + + Assert.Equal(cluster, hit.TextPosition); + + var hitRange = layout.HitTestTextRange(hit.TextPosition, 1); + + var distance = hitRange.First().Left; + + Assert.Equal(currentX, distance); + + currentX += advance; + } + + var secondRun = layout.TextLines[0].TextRuns[1] as ShapedTextCharacters; + + hit = layout.HitTestPoint(new Point(firstRun.Size.Width, 0)); + + Assert.Equal(7, hit.TextPosition); + + hit = layout.HitTestPoint(new Point(layout.TextLines[0].WidthIncludingTrailingWhitespace, 0)); + + Assert.Equal(0, hit.TextPosition); + + currentX = firstRun.Size.Width + 0.5; + + for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + { + var cluster = secondRun.GlyphRun.GlyphClusters[i]; + var advance = secondRun.GlyphRun.GlyphAdvances[i]; + + hit = layout.HitTestPoint(new Point(currentX, 0)); + + Assert.Equal(cluster, hit.CharacterHit.FirstCharacterIndex); + + var hitRange = layout.HitTestTextRange(hit.CharacterHit.FirstCharacterIndex, hit.CharacterHit.TrailingLength); + + var distance = hitRange.First().Left + 0.5; + + Assert.Equal(currentX, distance); + + currentX += advance; + } + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface From eb627f393cbfa1441cbdcefb52d79b2a2e82f070 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 11 Jul 2022 15:27:59 +0200 Subject: [PATCH 02/16] More fixes --- samples/Sandbox/App.axaml | 2 +- samples/Sandbox/MainWindow.axaml | 14 ++++- .../Media/TextFormatting/TextLayout.cs | 46 +++++++-------- .../Media/TextFormatting/TextLineImpl.cs | 58 +++++++++++-------- src/Avalonia.Controls/RichTextBlock.cs | 57 +++++++++--------- src/Avalonia.Controls/TextBlock.cs | 7 ++- .../Media/TextFormatting/TextLayoutTests.cs | 45 ++++++++++++++ .../Media/TextFormatting/TextLineTests.cs | 17 +++--- 8 files changed, 156 insertions(+), 90 deletions(-) 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 { From 7df34acb3d8e01c03fe39dc0fc3aec3c012c7b60 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 13 Jul 2022 12:14:10 +0200 Subject: [PATCH 03/16] Update src/Avalonia.Base/Media/GlyphRun.cs --- src/Avalonia.Base/Media/GlyphRun.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 703b56b0e8..da2143c188 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -652,7 +652,7 @@ namespace Avalonia.Media for (var index = 0; index < glyphCount; index++) { width -= GetGlyphAdvance(index, out _); - } + } } else { From 199d74332ae8cabb7c2ce3f56e5956372cd1d7d4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 13 Jul 2022 12:26:58 +0200 Subject: [PATCH 04/16] Add arabic test typeface --- .../Assets/NotoKufiArabic-Regular.ttf | Bin 0 -> 122736 bytes .../Media/CustomFontManagerImpl.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf diff --git a/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6d2ad86f947a530de5dec09b9a230948d0f512c3 GIT binary patch literal 122736 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 V75=m Date: Thu, 14 Jul 2022 06:22:53 +0200 Subject: [PATCH 05/16] Fix unit tests on mac --- .../Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs | 8 ++------ .../Media/TextFormatting/TextLayoutTests.cs | 8 ++++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index df286d709e..649e1fbf3d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -33,15 +33,11 @@ namespace Avalonia.Skia.UnitTests.Media { var fontManager = new FontManagerImpl(); - //we need to have a valid font name different from the default one - string fontName = fontManager.GetInstalledFontFamilyNames().First(); - var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily($"A, B, {fontName}"), weight: FontWeight.Bold)); + new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold)); var skTypeface = glyphTypeface.Typeface; - - Assert.Equal(fontName, skTypeface.FamilyName); + Assert.True(skTypeface.FontWeight >= 600); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 1241141ccf..7d33f094fa 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -925,7 +925,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; - var firstRun = textLine.TextRuns[0] as ShapedTextCharacters; + var firstRun = (ShapedTextCharacters)textLine.TextRuns[0]; var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; @@ -943,15 +943,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; - Assert.Equal(firstAdvance, distance); + Assert.Equal(firstAdvance, distance, 5); var rect = layout.HitTestTextPosition(22); - Assert.Equal(firstAdvance, rect.Left); + Assert.Equal(firstAdvance, rect.Left, 5); rect = layout.HitTestTextPosition(23); - Assert.Equal(0, rect.Left); + Assert.Equal(0, rect.Left, 5); } } From c921d1e097c1dced3b0ded4deb267929876a2fd9 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 22 Jul 2022 15:34:14 +0200 Subject: [PATCH 06/16] Fix center alignment for RTL --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 67c8f0c88c..f3c62f4994 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1317,8 +1317,14 @@ namespace Avalonia.Media.TextFormatting switch (textAlignment) { case TextAlignment.Center: - return Math.Max(0, (_paragraphWidth - width) / 2); + var start = (_paragraphWidth - width) / 2; + if(paragraphFlowDirection == FlowDirection.RightToLeft) + { + start -= (widthIncludingTrailingWhitespace - width); + } + + return Math.Max(0, start); case TextAlignment.Right: return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); From 6802722051223ed11055841fc39324db6f3c94ba Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 22 Jul 2022 16:13:21 +0200 Subject: [PATCH 07/16] Add whole word shift selection --- samples/Sandbox/Sandbox.csproj | 1 - src/Avalonia.Controls/RichTextBlock.cs | 33 ++++++++++++++++++++++---- src/Avalonia.Controls/TextBox.cs | 29 +++++++++++++++++++--- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index 20c7f29201..eab654acb6 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 6a40144137..3011cc7bd9 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -338,6 +338,8 @@ namespace Avalonia.Controls e.Handled = handled; } + private bool _hasWordSelection; + protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); @@ -363,8 +365,6 @@ namespace Avalonia.Controls var hit = TextLayout.HitTestPoint(point); var index = hit.TextPosition; - SelectionStart = SelectionEnd = index; - #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) #pragma warning restore CS0618 // Type or member is obsolete @@ -372,16 +372,27 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - SelectionStart = Math.Min(oldIndex, index); - SelectionEnd = Math.Max(oldIndex, index); + if (_hasWordSelection) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + else + { + SelectionStart = Math.Min(oldIndex, index); + SelectionEnd = Math.Max(oldIndex, index); + } } else { + _hasWordSelection = false; + SelectionStart = SelectionEnd = index; } break; case 2: + _hasWordSelection = true; + if (!StringUtils.IsStartOfWord(text, index)) { SelectionStart = StringUtils.PreviousWord(text, index); @@ -390,6 +401,8 @@ namespace Avalonia.Controls SelectionEnd = StringUtils.NextWord(text, index); break; case 3: + _hasWordSelection = false; + SelectAll(); break; } @@ -411,6 +424,7 @@ 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 text = Text; var padding = Padding; var point = e.GetPosition(this) - new Point(padding.Left, padding.Top); @@ -421,7 +435,16 @@ namespace Avalonia.Controls var hit = TextLayout.HitTestPoint(point); - SelectionEnd = hit.TextPosition; + if (text != null && _hasWordSelection) + { + SelectionEnd = StringUtils.NextWord(text, hit.TextPosition); + } + else + { + SelectionEnd = hit.TextPosition; + } + + } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9531f719b9..0946404f3b 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -202,6 +202,7 @@ namespace Avalonia.Controls private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; + private bool _hasWordSelection; private int _selectedTextChangesMadeSinceLastUndoSnapshot; private bool _hasDoneSnapshotOnce; private const int _maxCharsBeforeUndoSnapshot = 7; @@ -1170,16 +1171,27 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - SelectionStart = Math.Min(oldIndex, index); - SelectionEnd = Math.Max(oldIndex, index); + if (_hasWordSelection) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + else + { + SelectionStart = Math.Min(oldIndex, index); + SelectionEnd = Math.Max(oldIndex, index); + } } else { + _hasWordSelection = false; + SelectionStart = SelectionEnd = index; } break; case 2: + _hasWordSelection = true; + if (!StringUtils.IsStartOfWord(text, index)) { SelectionStart = StringUtils.PreviousWord(text, index); @@ -1188,6 +1200,8 @@ namespace Avalonia.Controls SelectionEnd = StringUtils.NextWord(text, index); break; case 3: + _hasWordSelection = false; + SelectAll(); break; } @@ -1215,7 +1229,16 @@ namespace Avalonia.Controls _presenter.MoveCaretToPoint(point); - SelectionEnd = _presenter.CaretIndex; + var text = Text; + + if (text != null && _hasWordSelection) + { + SelectionEnd = StringUtils.NextWord(text, _presenter.CaretIndex); + } + else + { + SelectionEnd = _presenter.CaretIndex; + } } } From c7182ae12adf019f9644a68c0361e7cc9987a0fc Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 22 Jul 2022 18:03:30 +0200 Subject: [PATCH 08/16] Improve advanced word selection --- src/Avalonia.Controls/RichTextBlock.cs | 53 +++-- src/Avalonia.Controls/TextBox.cs | 276 ++++++++++++++----------- 2 files changed, 193 insertions(+), 136 deletions(-) diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 3011cc7bd9..b7c5cfa3bd 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -60,6 +60,7 @@ namespace Avalonia.Controls private bool _canCopy; private int _selectionStart; private int _selectionEnd; + private int _wordSelectionStart = -1; static RichTextBlock() { @@ -338,8 +339,6 @@ namespace Avalonia.Controls e.Handled = handled; } - private bool _hasWordSelection; - protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); @@ -372,9 +371,19 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - if (_hasWordSelection) + if (_wordSelectionStart >= 0) { - SelectionEnd = StringUtils.NextWord(text, index); + var previousWord = StringUtils.PreviousWord(text, index); + + if (index > _wordSelectionStart) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + + if (index < _wordSelectionStart || previousWord == _wordSelectionStart) + { + SelectionStart = previousWord; + } } else { @@ -384,24 +393,27 @@ namespace Avalonia.Controls } else { - _hasWordSelection = false; + if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) + { + SelectionStart = SelectionEnd = index; - SelectionStart = SelectionEnd = index; + _wordSelectionStart = -1; + } } break; case 2: - _hasWordSelection = true; - if (!StringUtils.IsStartOfWord(text, index)) { SelectionStart = StringUtils.PreviousWord(text, index); } + _wordSelectionStart = SelectionStart; + SelectionEnd = StringUtils.NextWord(text, index); break; case 3: - _hasWordSelection = false; + _wordSelectionStart = -1; SelectAll(); break; @@ -434,17 +446,32 @@ namespace Avalonia.Controls MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0))); var hit = TextLayout.HitTestPoint(point); + var textPosition = hit.TextPosition; - if (text != null && _hasWordSelection) + if (text != null && _wordSelectionStart >= 0) { - SelectionEnd = StringUtils.NextWord(text, hit.TextPosition); + var distance = textPosition - _wordSelectionStart; + + if (distance <= 0) + { + SelectionStart = StringUtils.PreviousWord(text, textPosition); + } + + if (distance >= 0) + { + if (SelectionStart != _wordSelectionStart) + { + SelectionStart = _wordSelectionStart; + } + + SelectionEnd = StringUtils.NextWord(text, textPosition); + } } else { - SelectionEnd = hit.TextPosition; + SelectionEnd = textPosition; } - } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0946404f3b..1b268db2f7 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - + public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); @@ -80,7 +80,7 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxLinesProperty = AvaloniaProperty.Register(nameof(MaxLines), defaultValue: 0); - + public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwnerWithDataValidation( o => o.Text, @@ -105,7 +105,7 @@ namespace Avalonia.Controls public static readonly StyledProperty TextWrappingProperty = TextBlock.TextWrappingProperty.AddOwner(); - + /// /// Defines see property. /// @@ -202,7 +202,7 @@ namespace Avalonia.Controls private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; - private bool _hasWordSelection; + private int _wordSelectionStart = -1; private int _selectedTextChangesMadeSinceLastUndoSnapshot; private bool _hasDoneSnapshotOnce; private const int _maxCharsBeforeUndoSnapshot = 7; @@ -276,7 +276,7 @@ namespace Avalonia.Controls get => GetValue(IsReadOnlyProperty); set => SetValue(IsReadOnlyProperty, value); } - + public char PasswordChar { get => GetValue(PasswordCharProperty); @@ -308,7 +308,7 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value); - + if (changed) { UpdateCommandStates(); @@ -328,12 +328,12 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); - + if (changed) { UpdateCommandStates(); } - + if (SelectionStart == value && CaretIndex != value) { CaretIndex = value; @@ -352,7 +352,7 @@ namespace Avalonia.Controls get => GetValue(MaxLinesProperty); set => SetValue(MaxLinesProperty, value); } - + /// /// Gets or sets the line height. /// @@ -371,7 +371,7 @@ namespace Avalonia.Controls var caretIndex = CaretIndex; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; - + CaretIndex = CoerceCaretIndex(caretIndex, value); SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value); @@ -568,7 +568,7 @@ namespace Avalonia.Controls _presenter = e.NameScope.Get("PART_TextPresenter"); _imClient.SetPresenter(_presenter, this); - + if (IsFocused) { _presenter?.ShowCaret(); @@ -578,7 +578,7 @@ namespace Avalonia.Controls protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - + if (IsFocused) { _presenter?.ShowCaret(); @@ -588,7 +588,7 @@ namespace Avalonia.Controls protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - + _imClient.SetPresenter(null, null); } @@ -638,7 +638,7 @@ namespace Avalonia.Controls } UpdateCommandStates(); - + _imClient.SetPresenter(_presenter, this); _presenter?.ShowCaret(); @@ -658,7 +658,7 @@ namespace Avalonia.Controls UpdateCommandStates(); _presenter?.HideCaret(); - + _imClient.SetPresenter(null, null); } @@ -701,14 +701,14 @@ namespace Avalonia.Controls if (grapheme.FirstCodepoint.IsBreakChar) { - if(lineCount + 1 > MaxLines) + if (lineCount + 1 > MaxLines) { break; } else { lineCount++; - } + } } length += grapheme.Text.Length; @@ -737,7 +737,7 @@ namespace Avalonia.Controls text = Text ?? string.Empty; SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); ClearSelection(); - + if (IsUndoEnabled) { _undoRedoHelper.DiscardRedo(); @@ -747,7 +747,7 @@ namespace Avalonia.Controls { RaisePropertyChanged(TextProperty, oldText, _text); } - + CaretIndex = caretIndex + input.Length; } } @@ -829,7 +829,7 @@ namespace Avalonia.Controls { return; } - + var text = Text ?? string.Empty; var caretIndex = CaretIndex; var movement = false; @@ -986,87 +986,87 @@ namespace Avalonia.Controls break; case Key.Up: - { - selection = DetectSelection(); - - _presenter.MoveCaretVertical(LogicalDirection.Backward); - - if (caretIndex != _presenter.CaretIndex) { - movement = true; - } + selection = DetectSelection(); - if (selection) - { - SelectionEnd = _presenter.CaretIndex; - } - else - { - CaretIndex = _presenter.CaretIndex; + _presenter.MoveCaretVertical(LogicalDirection.Backward); + + if (caretIndex != _presenter.CaretIndex) + { + movement = true; + } + + if (selection) + { + SelectionEnd = _presenter.CaretIndex; + } + else + { + CaretIndex = _presenter.CaretIndex; + } + + break; } - - break; - } case Key.Down: - { - selection = DetectSelection(); - - _presenter.MoveCaretVertical(); - - if (caretIndex != _presenter.CaretIndex) - { - movement = true; - } - - if (selection) { - SelectionEnd = _presenter.CaretIndex; - } - else - { - CaretIndex = _presenter.CaretIndex; + selection = DetectSelection(); + + _presenter.MoveCaretVertical(); + + if (caretIndex != _presenter.CaretIndex) + { + movement = true; + } + + if (selection) + { + SelectionEnd = _presenter.CaretIndex; + } + else + { + CaretIndex = _presenter.CaretIndex; + } + + break; } - - break; - } case Key.Back: - { - SnapshotUndoRedo(); - - if (hasWholeWordModifiers && SelectionStart == SelectionEnd) - { - SetSelectionForControlBackspace(); - } - - if (!DeleteSelection()) { - var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); + SnapshotUndoRedo(); - var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) + { + SetSelectionForControlBackspace(); + } - if (caretIndex != backspacePosition) + if (!DeleteSelection()) { - var start = Math.Min(backspacePosition, caretIndex); - var end = Math.Max(backspacePosition, caretIndex); + var characterHit = _presenter.GetNextCharacterHit(LogicalDirection.Backward); - var length = end - start; + var backspacePosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); + if (caretIndex != backspacePosition) + { + var start = Math.Min(backspacePosition, caretIndex); + var end = Math.Max(backspacePosition, caretIndex); - SetTextInternal(editedText); + var length = end - start; - CaretIndex = start; - } - } - - SnapshotUndoRedo(); + var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length)); - handled = true; - break; - } + SetTextInternal(editedText); + + CaretIndex = start; + } + } + + SnapshotUndoRedo(); + + handled = true; + break; + } case Key.Delete: SnapshotUndoRedo(); - + if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); @@ -1078,7 +1078,7 @@ namespace Avalonia.Controls var nextPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if(nextPosition != caretIndex) + if (nextPosition != caretIndex) { var start = Math.Min(nextPosition, caretIndex); var end = Math.Max(nextPosition, caretIndex); @@ -1145,7 +1145,7 @@ namespace Avalonia.Controls { return; } - + var text = Text; var clickInfo = e.GetCurrentPoint(this); @@ -1171,36 +1171,49 @@ namespace Avalonia.Controls case 1: if (clickToSelect) { - if (_hasWordSelection) + if (_wordSelectionStart >= 0) { - SelectionEnd = StringUtils.NextWord(text, index); + var previousWord = StringUtils.PreviousWord(text, index); + + if (index > _wordSelectionStart) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + + if (index < _wordSelectionStart || previousWord == _wordSelectionStart) + { + SelectionStart = previousWord; + } } else { SelectionStart = Math.Min(oldIndex, index); SelectionEnd = Math.Max(oldIndex, index); - } + } } else { - _hasWordSelection = false; - - SelectionStart = SelectionEnd = index; + if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd) + { + SelectionStart = SelectionEnd = index; + _wordSelectionStart = -1; + } } break; - case 2: - _hasWordSelection = true; + case 2: if (!StringUtils.IsStartOfWord(text, index)) { SelectionStart = StringUtils.PreviousWord(text, index); } + _wordSelectionStart = SelectionStart; + SelectionEnd = StringUtils.NextWord(text, index); break; case 3: - _hasWordSelection = false; + _wordSelectionStart = -1; SelectAll(); break; @@ -1217,7 +1230,7 @@ namespace Avalonia.Controls { return; } - + // selection should not change during pointer move if the user right clicks if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { @@ -1229,11 +1242,27 @@ namespace Avalonia.Controls _presenter.MoveCaretToPoint(point); + var caretIndex = _presenter.CaretIndex; var text = Text; - if (text != null && _hasWordSelection) + if (text != null && _wordSelectionStart >= 0) { - SelectionEnd = StringUtils.NextWord(text, _presenter.CaretIndex); + var distance = caretIndex - _wordSelectionStart; + + if (distance <= 0) + { + SelectionStart = StringUtils.PreviousWord(text, caretIndex); + } + + if (distance >= 0) + { + if(SelectionStart != _wordSelectionStart) + { + SelectionStart = _wordSelectionStart; + } + + SelectionEnd = StringUtils.NextWord(text, caretIndex); + } } else { @@ -1257,9 +1286,9 @@ namespace Avalonia.Controls if (e.InitialPressMouseButton == MouseButton.Right) { var point = e.GetPosition(_presenter); - + _presenter.MoveCaretToPoint(point); - + var caretIndex = _presenter.CaretIndex; // see if mouse clicked inside current selection @@ -1273,7 +1302,7 @@ namespace Avalonia.Controls CaretIndex = SelectionEnd = SelectionStart = caretIndex; } } - + e.Pointer.Capture(null); } @@ -1332,7 +1361,7 @@ namespace Avalonia.Controls { return; } - + var text = Text ?? string.Empty; var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -1342,11 +1371,11 @@ namespace Avalonia.Controls if (isSelecting) { _presenter.MoveCaretToTextPosition(selectionEnd); - + _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward); - + SelectionEnd = _presenter.CaretIndex; } else @@ -1370,7 +1399,7 @@ namespace Avalonia.Controls else { int offset; - + if (direction > 0) { offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd; @@ -1379,7 +1408,7 @@ namespace Avalonia.Controls { offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd; } - + SelectionEnd += offset; _presenter.MoveCaretToTextPosition(SelectionEnd); @@ -1401,7 +1430,7 @@ namespace Avalonia.Controls { return; } - + var caretIndex = CaretIndex; if (document) @@ -1424,7 +1453,7 @@ namespace Avalonia.Controls { return; } - + var text = Text ?? string.Empty; var caretIndex = CaretIndex; @@ -1455,8 +1484,9 @@ namespace Avalonia.Controls private bool DeleteSelection(bool raiseTextChanged = true) { - if (IsReadOnly) return true; - + if (IsReadOnly) + return true; + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -1467,40 +1497,40 @@ namespace Avalonia.Controls var text = Text!; SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged); - + _presenter?.MoveCaretToTextPosition(start); - - CaretIndex= start; - + + CaretIndex = start; + ClearSelection(); - + return true; } - + CaretIndex = SelectionStart; - + return false; } private string GetSelection() { var text = Text; - + if (string.IsNullOrEmpty(text)) { return ""; } - + var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); - + if (start == end || (Text?.Length ?? 0) < end) { return ""; } - + return text.Substring(start, end - start); } @@ -1519,7 +1549,7 @@ namespace Avalonia.Controls private void SetSelectionForControlBackspace() { var selectionStart = CaretIndex; - + MoveHorizontal(-1, true, false); SelectionStart = selectionStart; @@ -1531,9 +1561,9 @@ namespace Avalonia.Controls { return; } - + SelectionStart = CaretIndex; - + MoveHorizontal(1, true, true); if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ') From 9f37910c97c8401e4694a7be9dbdbc1cf6890456 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 22 Jul 2022 18:43:49 +0200 Subject: [PATCH 09/16] Fix TextDecoration width Fix LineBreak length --- samples/Sandbox/MainWindow.axaml | 14 +------------- src/Avalonia.Base/Media/GlyphRun.cs | 2 +- src/Avalonia.Base/Media/TextDecoration.cs | 2 +- src/Avalonia.Controls/Documents/LineBreak.cs | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 957616579a..c305c57efb 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,17 +1,5 @@ - - - أَبْجَدِيَّة عَرَبِيَّة - + diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index da2143c188..2a7f3360ad 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -445,7 +445,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null) + if (GlyphClusters == null || GlyphClusters.Count == 0) { return characterIndex; } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 8eeb86c555..4c9764af96 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -209,7 +209,7 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); - drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Size.Width, 0)); + drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); } } } diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index aeb81f7313..ef8bd35556 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -22,7 +21,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - textRuns.Add(new TextEndOfLine()); + textRuns.Add(new TextEndOfLine(Environment.NewLine.Length)); } internal override void AppendText(StringBuilder stringBuilder) From 0372dd914cf45d70b21144ee046b58eddc432bea Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 22 Jul 2022 19:16:10 +0200 Subject: [PATCH 10/16] Show IBeam cursor for IsTextSelectionEnabled --- src/Avalonia.Controls/RichTextBlock.cs | 2 +- .../Avalonia.Themes.Default.csproj | 8 ++++++++ .../Controls/RichTextBlock.xaml | 10 ++++++++++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + .../Avalonia.Themes.Fluent.csproj | 8 ++++++++ .../Controls/FluentControls.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml | 10 ++++++++++ 7 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 5006e8133b..0c8b1d125d 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls { FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); - AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty); + AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionBrushProperty, IsTextSelectionEnabledProperty); } public RichTextBlock() diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index 40ed4a0f87..ae0df89c60 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -9,6 +9,14 @@ + + + + + + MSBuild:Compile + + diff --git a/src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml new file mode 100644 index 0000000000..d7bf6e5cf9 --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/RichTextBlock.xaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 468b723f5b..f266402aef 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -66,4 +66,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 35603fe216..8acf12a0ff 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,6 +10,14 @@ + + + + + + MSBuild:Compile + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5b217e4764..eb2d65150c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -67,4 +67,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml new file mode 100644 index 0000000000..d7bf6e5cf9 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml @@ -0,0 +1,10 @@ + + + + + + + From 03b8b2eb5eb1e72523c7edbf6daf48f9f5e74839 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 22 Jul 2022 19:45:25 +0200 Subject: [PATCH 11/16] Show ellipsis when MaxLines are reached --- samples/Sandbox/MainWindow.axaml | 1 - src/Avalonia.Base/Media/TextFormatting/TextLayout.cs | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index c305c57efb..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,5 +1,4 @@ - diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 8ab9591faf..f3e8b5969c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -479,6 +479,11 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { + if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) + { + textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + } + break; } } From 4ff9225f002fa518ef16f0ab16567cf3c82b03ab Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Jul 2022 11:08:57 +0200 Subject: [PATCH 12/16] Actually add Environment.NewLine to the TextLayout for LineBreak runs --- src/Avalonia.Controls/Documents/LineBreak.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index ef8bd35556..108a38d86b 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -21,7 +21,13 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - textRuns.Add(new TextEndOfLine(Environment.NewLine.Length)); + var text = Environment.NewLine.AsMemory(); + + var textRunProperties = CreateTextRunProperties(); + + var textCharacters = new TextCharacters(text, textRunProperties); + + textRuns.Add(textCharacters); } internal override void AppendText(StringBuilder stringBuilder) From 127e77a5433f92683c9697d73511b2ba94b226ff Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Jul 2022 12:31:24 +0200 Subject: [PATCH 13/16] Update RichTextBlock ControlTheme --- .../Controls/FluentControls.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index bc2352d5d0..0499495239 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -68,6 +68,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml index d7bf6e5cf9..75af2efcb1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RichTextBlock.xaml @@ -1,10 +1,14 @@ - - - + + + + From a4679604981a1840b37e99578405ecfe776caf0f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Jul 2022 12:32:51 +0200 Subject: [PATCH 14/16] Revert change --- samples/Sandbox/App.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml index 1c74c07b1d..f601f9f78f 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"> - + From fdd8e5bdb92e213d1295e95229ac81d3e779e3b8 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Jul 2022 12:33:48 +0200 Subject: [PATCH 15/16] Revert change --- .../Avalonia.Themes.Default.csproj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index ae0df89c60..40ed4a0f87 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -9,14 +9,6 @@ - - - - - - MSBuild:Compile - - From 3e39ace557fd448814e508660afc301b90a15aa3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Jul 2022 12:34:12 +0200 Subject: [PATCH 16/16] Revert change --- src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 8acf12a0ff..35603fe216 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,14 +10,6 @@ - - - - - - MSBuild:Compile - -