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); + } } }