From e8719e018db82a6998900e61bea3f11470eade9f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:24:53 +0200 Subject: [PATCH 1/4] Fix GlyphRun.GetTralingWhitespaceLengthRightToLeft --- src/Avalonia.Base/Media/GlyphRun.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index cae7a8fe75..f207b3c636 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -786,14 +786,15 @@ namespace Avalonia.Media var clusterLength = 1; - while (i - 1 >= 0) + var j = i; + + while (j - 1 >= 0) { - var nextCluster = GlyphClusters[i - 1]; + var nextCluster = GlyphClusters[--j]; if (currentCluster == nextCluster) { - clusterLength++; - i--; + clusterLength++; continue; } @@ -808,7 +809,7 @@ namespace Avalonia.Media trailingWhitespaceLength += clusterLength; - glyphCount++; + glyphCount += clusterLength; } } From 0856cfaff985c15614b3bd647cf827b021a0444a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:25:12 +0200 Subject: [PATCH 2/4] Remove redundant comment --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index fa1ab6fd29..7495956cd2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -266,10 +266,6 @@ namespace Avalonia.Media.TextFormatting { offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } - //else - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); From 678620422df2e6bea1b9c7e3832a998559af2083 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 19 Aug 2022 15:59:51 +0200 Subject: [PATCH 3/4] More RTL hit testing fixes --- .../Media/TextFormatting/TextLineImpl.cs | 249 ++++++++---------- .../Media/TextFormatting/TextRunBounds.cs | 2 +- .../Documents/InlineCollection.cs | 14 +- .../Media/TextFormatting/TextLineTests.cs | 83 +++++- 4 files changed, 202 insertions(+), 146 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7495956cd2..aba8008fb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; - if(collapsingProperties is null) + if (collapsingProperties is null) { return this; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[i]; - if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; currentPosition += currentRun.TextSourceLength; @@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if(j > _textRuns.Count - 1) + if (j > _textRuns.Count - 1) { break; } currentRun = _textRuns[j]; - if(currentDistance + currentRun.Size.Width <= distance) + if (currentDistance + currentRun.Size.Width <= distance) { currentDistance += currentRun.Size.Width; currentPosition -= currentRun.TextSourceLength; @@ -322,11 +322,11 @@ namespace Avalonia.Media.TextFormatting continue; } - + break; } - if(i > index) + if (i > index) { while (i >= index) { @@ -350,7 +350,7 @@ namespace Avalonia.Media.TextFormatting } } - if (currentPosition + currentRun.TextSourceLength >= characterIndex && + if (currentPosition + currentRun.TextSourceLength >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return Math.Max(0, currentDistance + distance); @@ -530,6 +530,8 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = Rect.Empty; + TextRunBounds lastRunBounds = default; + for (var index = 0; index < TextRuns.Count; index++) { if (TextRuns[index] is not DrawableTextRun currentRun) @@ -539,53 +541,93 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - var runWidth = 0.0; - TextRunBounds? currentRunBounds = null; var currentShapedRun = currentRun as ShapedTextCharacters; + TextRunBounds currentRunBounds; + + double combinedWidth; + + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = index; - startX += currentShapedRun.Size.Width; + var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) { - var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { break; } - startX += nextShapedRun.Size.Width; - rightToLeftIndex++; + + rightToLeftWidth += nextShapedRun.Size.Width; + + if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength) + { + break; + } + + currentShapedRun = nextShapedRun; } - if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) + startX = startX + rightToLeftWidth; + + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + remainingLength -= currentRunBounds.Length; + currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; + endX = currentRunBounds.Rectangle.Right; + startX = currentRunBounds.Rectangle.Left; + + var rightToLeftRunBounds = new List { currentRunBounds }; + + for (int i = rightToLeftIndex - 1; i >= index; i--) { - startX = currentRunBounds!.Rectangle.Left; - endX = currentRunBounds.Rectangle.Right; + currentShapedRun = TextRuns[i] as ShapedTextCharacters; + + if(currentShapedRun == null) + { + continue; + } - runWidth = currentRunBounds.Rectangle.Width; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + rightToLeftRunBounds.Insert(0, currentRunBounds); + + remainingLength -= currentRunBounds.Length; + startX = currentRunBounds.Rectangle.Left; + + currentPosition += currentRunBounds.Length; } + combinedWidth = endX - startX; + + currentRect = new Rect(startX, 0, combinedWidth, Height); + currentDirection = FlowDirection.RightToLeft; + + if (!MathUtilities.IsZero(combinedWidth)) + { + result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); + } + + startX = endX; } else { if (currentShapedRun != null) { - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); currentPosition += offset; @@ -661,43 +703,46 @@ namespace Avalonia.Media.TextFormatting characterLength = NewLineLength; } - runWidth = endX - startX; - currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + combinedWidth = endX - startX; + + currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); currentPosition += characterLength; remainingLength -= characterLength; - } - if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + startX = endX; + + if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + { + currentRect = currentRect.WithWidth(currentWidth + combinedWidth); - 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 })); + } } + + lastRunBounds = currentRunBounds; } - currentWidth += runWidth; - + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) { break; } - startX = endX; lastDirection = currentDirection; } @@ -852,105 +897,45 @@ namespace Avalonia.Media.TextFormatting return result; } - private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) + private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) { - textRunBounds = null; + var startX = endX; - for (var index = runIndex; index >= 0; index--) - { - if (TextRuns[index] is not DrawableTextRun currentRun) - { - continue; - } + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += offset; - currentPosition += currentRun.TextSourceLength; + var startIndex = currentRun.Text.Start + offset; - continue; - } + double startOffset; + double endOffset; - var characterLength = 0; - var endX = startX; - - if (currentRun is ShapedTextCharacters currentShapedRun) - { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; - - var startIndex = currentRun.Text.Start + offset; - double startOffset; - double endOffset; - - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - } - - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; - - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - } - else - { - if (currentPosition + currentRun.TextSourceLength <= characterIndex) - { - endX -= currentRun.Size.Width; - } + startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; - characterLength = currentRun.TextSourceLength; - } - } + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } - - var runWidth = endX - startX; - - remainingLength -= characterLength; - - currentPosition += characterLength; - - textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + if (endX < startX) + { + (endX, startX) = (startX, endX); + } - return true; + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - return false; + var runWidth = endX - startX; + + return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) @@ -1532,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; - if(textAlignment == TextAlignment.Justify) + if (textAlignment == TextAlignment.Justify) { textAlignment = TextAlignment.Start; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index 91150160ed..bdc7a1ca89 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public sealed class TextRunBounds + public readonly struct TextRunBounds { /// /// Constructing TextRunBounds diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 565ed75ad9..15b4688809 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents private void AddText(string text) { - if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) + if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) { textBlock._text += text; } @@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents { foreach (var child in this) { - ((ISetLogicalParent)child).SetParent(parent); + var oldParent = child.Parent; + + if (oldParent != parent) + { + if (oldParent != null) + { + ((ISetLogicalParent)child).SetParent(null); + } + + ((ISetLogicalParent)child).SetParent(parent); + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d744ede87d..251c850fc8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -597,21 +597,82 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(1, textBounds.Count); + Assert.Equal(2, textBounds.Count); - Assert.Equal(144.0234375, textBounds[0].Rectangle.Width); + Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(1, textBounds.Count); + Assert.Equal(3, textBounds.Count); - Assert.Equal(216.03515625, textBounds[0].Rectangle.Width); + Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(1, textBounds.Count); + Assert.Equal(4, textBounds.Count); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_GetTextRange() + { + var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textRuns = textLine.TextRuns.Cast().ToList(); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width); + var lineWidth = textLine.WidthIncludingTrailingWhitespace; + + var textBounds = textLine.GetTextBounds(0, text.Length); + + TextBounds lastBounds = null; + + var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList(); + + Assert.Equal(textRuns.Count, runBounds.Count); + + for (var i = 0; i < textRuns.Count; i++) + { + var run = textRuns[i]; + var bounds = runBounds[i]; + + Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run, bounds.TextRun); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + } + + for (var i = 0; i < textBounds.Count; i++) + { + var currentBounds = textBounds[i]; + + if (lastBounds != null) + { + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + } + + var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + + lastBounds = currentBounds; + } + + var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(lineWidth, sumOfBoundsWidth); } } @@ -779,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); - Assert.Equal(1, textBounds.Count); + Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -789,8 +850,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); @@ -799,8 +860,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } From 116fd47439a8d4b49567c6bd1b8c35b56cc62e1a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 28 Sep 2022 06:36:17 +0200 Subject: [PATCH 4/4] Implement FormattedText.BuildHighlightGeometry --- .../Pages/FormattedTextPage.axaml.cs | 5 + samples/Sandbox/MainWindow.axaml | 1 - src/Avalonia.Base/Media/FormattedText.cs | 106 +++++++++++++++++- src/Avalonia.Base/Media/Geometry.cs | 13 +++ 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs index 97a9320c95..088a063690 100644 --- a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs +++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Immutable; namespace RenderDemo.Pages { @@ -59,6 +60,10 @@ namespace RenderDemo.Pages var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0)); context.DrawGeometry(gradient, null, geometry); + + var highlightGeometry = formattedText.BuildHighlightGeometry(new Point(10 + formattedText.Width + 10, 0)); + + context.DrawGeometry(null, new ImmutablePen(gradient.ToImmutable(), 2), highlightGeometry); } } } diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 43d93a9315..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,5 +1,4 @@ - diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 5480336f84..27d99bdc10 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1,8 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; @@ -654,14 +656,16 @@ namespace Avalonia.Media // line break before _currentLine, needed in case we have to reformat it with collapsing symbol private TextLineBreak? _previousLineBreak; + private int _position; + private int _length; internal LineEnumerator(FormattedText text) { _previousHeight = 0; - Length = 0; + _length = 0; _previousLineBreak = null; - Position = 0; + _position = 0; _lineCount = 0; _totalHeight = 0; Current = null; @@ -678,9 +682,17 @@ namespace Avalonia.Media _nextLine = null; } - private int Position { get; set; } + public int Position + { + get => _position; + private set => _position = value; + } - private int Length { get; set; } + public int Length + { + get => _length; + private set => _length = value; + } /// /// Gets the current text line in the collection @@ -1292,6 +1304,92 @@ namespace Avalonia.Media return accumulatedGeometry; } + /// + /// Builds a highlight geometry object. + /// + /// The origin of the highlight region + /// Geometry that surrounds the text. + public Geometry? BuildHighlightGeometry(Point origin) + { + return BuildHighlightGeometry(origin, 0, _text.Length); + } + + /// + /// Builds a highlight geometry object for a given character range. + /// + /// The origin of the highlight region. + /// The start index of initial character the bounds should be obtained for. + /// The number of characters the bounds should be obtained for. + /// Geometry that surrounds the specified character range. + public Geometry? BuildHighlightGeometry(Point origin, int startIndex, int count) + { + ValidateRange(startIndex, count); + + Geometry? accumulatedBounds = null; + + using (var enumerator = GetEnumerator()) + { + var lineOrigin = origin; + + while (enumerator.MoveNext()) + { + var currentLine = enumerator.Current!; + + int x0 = Math.Max(enumerator.Position, startIndex); + int x1 = Math.Min(enumerator.Position + enumerator.Length, startIndex + count); + + // check if this line is intersects with the specified character range + if (x0 < x1) + { + var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0); + + if (highlightBounds != null) + { + foreach (var bound in highlightBounds) + { + var rect = bound.Rectangle; + + if (FlowDirection == FlowDirection.RightToLeft) + { + // Convert logical units (which extend leftward from the right edge + // of the paragraph) to physical units. + // + // Note that since rect is in logical units, rect.Right corresponds to + // the visual *left* edge of the rectangle in the RTL case. Specifically, + // is the distance leftward from the right edge of the formatting rectangle + // whose width is the paragraph width passed to FormatLine. + // + rect = rect.WithX(enumerator.CurrentParagraphWidth - rect.Right); + } + + rect = new Rect(new Point(rect.X + lineOrigin.X, rect.Y + lineOrigin.Y), rect.Size); + + RectangleGeometry rectangleGeometry = new RectangleGeometry(rect); + + if (accumulatedBounds == null) + { + accumulatedBounds = rectangleGeometry; + } + else + { + accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union); + } + } + } + } + + AdvanceLineOrigin(ref lineOrigin, currentLine); + } + } + + if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty) + { + return null; + } + + return accumulatedBounds; + } + /// /// Draws the text object /// diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index 76c67a5cf4..c31a6699c2 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -185,5 +185,18 @@ namespace Avalonia.Media var control = e.Sender as Geometry; control?.InvalidateGeometry(); } + + /// + /// Combines the two geometries using the specified and applies the specified transform to the resulting geometry. + /// + /// The first geometry to combine. + /// The second geometry to combine. + /// One of the enumeration values that specifies how the geometries are combined. + /// A transformation to apply to the combined geometry, or null. + /// + public static Geometry Combine(Geometry geometry1, RectangleGeometry geometry2, GeometryCombineMode combineMode, Transform? transform = null) + { + return new CombinedGeometry(combineMode, geometry1, geometry2, transform); + } } }