Преглед изворни кода

Text API sample app (#19455)

* xml documentatation for paragraph properties

* TextTestApp

* text test files renamed parameter

* glyph ink bounds

* shaped buffer make selected row visible

* text test app force light theme

* text test app distinguish start and end distance marks

---------

Co-authored-by: Jan Kučera <[email protected]>
Co-authored-by: Benedikt Stebner <[email protected]>
Co-authored-by: Julien Lebosquain <[email protected]>
Jan Kučera пре 2 месеци
родитељ
комит
21b5812746

+ 1 - 0
Avalonia.Desktop.slnf

@@ -8,6 +8,7 @@
       "samples\\ControlCatalog\\ControlCatalog.csproj",
       "samples\\GpuInterop\\GpuInterop.csproj",
       "samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
+      "samples\\TextTestApp\\TextTestApp.csproj",
       "samples\\MiniMvvm\\MiniMvvm.csproj",
       "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
       "samples\\RenderDemo\\RenderDemo.csproj",

+ 7 - 0
Avalonia.sln

@@ -191,6 +191,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvv
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextTestApp", "samples\TextTestApp\TextTestApp.csproj", "{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}"
+EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Browser", "Browser", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
@@ -538,6 +540,10 @@ Global
 		{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.Build.0 = Release|Any CPU
 		{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -761,6 +767,7 @@ Global
 		{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+		{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
 		{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}

+ 5 - 0
samples/TextTestApp/App.axaml

@@ -0,0 +1,5 @@
+<Application xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="TextTestApp.App" RequestedThemeVariant="Light">
+  <Application.Styles>
+    <FluentTheme />
+  </Application.Styles>
+</Application>

+ 21 - 0
samples/TextTestApp/App.axaml.cs

@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace TextTestApp
+{
+    public partial class App : Application
+    {
+        public override void Initialize()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        public override void OnFrameworkInitializationCompleted()
+        {
+            if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+                desktop.MainWindow = new MainWindow();
+            base.OnFrameworkInitializationCompleted();
+        }
+    }
+}

+ 24 - 0
samples/TextTestApp/GridRow.cs

@@ -0,0 +1,24 @@
+using System.Collections.Specialized;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace TextTestApp
+{
+    public class GridRow : Grid
+    {
+        protected override void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
+        {
+            base.ChildrenChanged(sender, e);
+
+            while (Children.Count > ColumnDefinitions.Count)
+                ColumnDefinitions.Add(new ColumnDefinition { SharedSizeGroup = "c" + ColumnDefinitions.Count });
+
+            for (int i = 0; i < Children.Count; i++)
+            {
+                SetColumn(Children[i], i);
+                if (Children[i] is Layoutable l)
+                    l.VerticalAlignment = VerticalAlignment.Center;
+            }
+        }
+    }
+}

+ 705 - 0
samples/TextTestApp/InteractiveLineControl.cs

@@ -0,0 +1,705 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Controls.Primitives;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+
+namespace TextTestApp
+{
+    public class InteractiveLineControl : Control
+    {
+        /// <summary>
+        /// Defines the <see cref="Text" /> property.
+        /// </summary>
+        public static readonly StyledProperty<string?> TextProperty =
+            TextBlock.TextProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Defines the <see cref="Background"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IBrush?> BackgroundProperty =
+            Border.BackgroundProperty.AddOwner<InteractiveLineControl>();
+
+        public static readonly StyledProperty<IBrush?> ExtentStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(ExtentStroke));
+
+        public static readonly StyledProperty<IBrush?> BaselineStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BaselineStroke));
+
+        public static readonly StyledProperty<IBrush?> TextBoundsStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(TextBoundsStroke));
+
+        public static readonly StyledProperty<IBrush?> RunBoundsStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(RunBoundsStroke));
+
+        public static readonly StyledProperty<IBrush?> NextHitStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(NextHitStroke));
+
+        public static readonly StyledProperty<IBrush?> BackspaceHitStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(BackspaceHitStroke));
+
+        public static readonly StyledProperty<IBrush?> PreviousHitStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(PreviousHitStroke));
+
+        public static readonly StyledProperty<IBrush?> DistanceStrokeProperty =
+            AvaloniaProperty.Register<InteractiveLineControl, IBrush?>(nameof(DistanceStroke));
+
+        public IBrush? ExtentStroke
+        {
+            get => GetValue(ExtentStrokeProperty);
+            set => SetValue(ExtentStrokeProperty, value);
+        }
+        public IBrush? BaselineStroke
+        {
+            get => GetValue(BaselineStrokeProperty);
+            set => SetValue(BaselineStrokeProperty, value);
+        }
+
+        public IBrush? TextBoundsStroke
+        {
+            get => GetValue(TextBoundsStrokeProperty);
+            set => SetValue(TextBoundsStrokeProperty, value);
+        }
+
+        public IBrush? RunBoundsStroke
+        {
+            get => GetValue(RunBoundsStrokeProperty);
+            set => SetValue(RunBoundsStrokeProperty, value);
+        }
+
+        public IBrush? NextHitStroke
+        {
+            get => GetValue(NextHitStrokeProperty);
+            set => SetValue(NextHitStrokeProperty, value);
+        }
+
+        public IBrush? BackspaceHitStroke 
+        {
+            get => GetValue(BackspaceHitStrokeProperty);
+            set => SetValue(BackspaceHitStrokeProperty, value);
+        }
+
+        public IBrush? PreviousHitStroke
+        {
+            get => GetValue(PreviousHitStrokeProperty);
+            set => SetValue(PreviousHitStrokeProperty, value);
+        }
+
+        public IBrush? DistanceStroke 
+        {
+            get => GetValue(DistanceStrokeProperty);
+            set => SetValue(DistanceStrokeProperty, value);
+        }
+
+        private IPen? _extentPen;
+        protected IPen ExtentPen => _extentPen ??= new Pen(ExtentStroke, dashStyle: DashStyle.Dash);
+
+        private IPen? _baselinePen;
+        protected IPen BaselinePen => _baselinePen ??= new Pen(BaselineStroke);
+
+        private IPen? _textBoundsPen;
+        protected IPen TextBoundsPen => _textBoundsPen ??= new Pen(TextBoundsStroke);
+
+        private IPen? _runBoundsPen;
+        protected IPen RunBoundsPen => _runBoundsPen ??= new Pen(RunBoundsStroke, dashStyle: DashStyle.Dash);
+
+        private IPen? _nextHitPen;
+        protected IPen NextHitPen => _nextHitPen ??= new Pen(NextHitStroke);
+
+        private IPen? _previousHitPen;
+        protected IPen PreviousHitPen => _previousHitPen ??= new Pen(PreviousHitStroke);
+
+        private IPen? _backspaceHitPen;
+        protected IPen BackspaceHitPen => _backspaceHitPen ??= new Pen(BackspaceHitStroke);
+
+        private IPen? _distancePen;
+        protected IPen DistancePen => _distancePen ??= new Pen(DistanceStroke);
+
+        /// <summary>
+        /// Gets or sets the text to draw.
+        /// </summary>
+        public string? Text
+        {
+            get => GetValue(TextProperty);
+            set => SetValue(TextProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a brush used to paint the control's background.
+        /// </summary>
+        public IBrush? Background
+        {
+            get => GetValue(BackgroundProperty);
+            set => SetValue(BackgroundProperty, value);
+        }
+
+        // TextRunProperties
+
+        /// <summary>
+        /// Defines the <see cref="FontFamily"/> property.
+        /// </summary>
+        public static readonly StyledProperty<FontFamily> FontFamilyProperty =
+            TextElement.FontFamilyProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Defines the <see cref="FontFeaturesProperty"/> property.
+        /// </summary>
+        public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty =
+            TextElement.FontFeaturesProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Defines the <see cref="FontSize"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> FontSizeProperty =
+            TextElement.FontSizeProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Defines the <see cref="FontStyle"/> property.
+        /// </summary>
+        public static readonly StyledProperty<FontStyle> FontStyleProperty =
+            TextElement.FontStyleProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Defines the <see cref="FontWeight"/> property.
+        /// </summary>
+        public static readonly StyledProperty<FontWeight> FontWeightProperty =
+            TextElement.FontWeightProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Defines the <see cref="FontWeight"/> property.
+        /// </summary>
+        public static readonly StyledProperty<FontStretch> FontStretchProperty =
+            TextElement.FontStretchProperty.AddOwner<InteractiveLineControl>();
+
+        /// <summary>
+        /// Gets or sets the font family used to draw the control's text.
+        /// </summary>
+        public FontFamily FontFamily
+        {
+            get => GetValue(FontFamilyProperty);
+            set => SetValue(FontFamilyProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font features turned on/off.
+        /// </summary>
+        public FontFeatureCollection? FontFeatures
+        {
+            get => GetValue(FontFeaturesProperty);
+            set => SetValue(FontFeaturesProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the size of the control's text in points.
+        /// </summary>
+        public double FontSize
+        {
+            get => GetValue(FontSizeProperty);
+            set => SetValue(FontSizeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font style used to draw the control's text.
+        /// </summary>
+        public FontStyle FontStyle
+        {
+            get => GetValue(FontStyleProperty);
+            set => SetValue(FontStyleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font weight used to draw the control's text.
+        /// </summary>
+        public FontWeight FontWeight
+        {
+            get => GetValue(FontWeightProperty);
+            set => SetValue(FontWeightProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font stretch used to draw the control's text.
+        /// </summary>
+        public FontStretch FontStretch
+        {
+            get => GetValue(FontStretchProperty);
+            set => SetValue(FontStretchProperty, value);
+        }
+
+        private GenericTextRunProperties? _textRunProperties;
+        public GenericTextRunProperties TextRunProperties
+        {
+            get
+            {
+                return _textRunProperties ??= CreateTextRunProperties();
+            }
+            set
+            {
+                if (value == null)
+                    throw new ArgumentNullException(nameof(value));
+
+                _textRunProperties = value;
+                SetCurrentValue(FontFamilyProperty, value.Typeface.FontFamily);
+                SetCurrentValue(FontFeaturesProperty, value.FontFeatures);
+                SetCurrentValue(FontSizeProperty, value.FontRenderingEmSize);
+                SetCurrentValue(FontStyleProperty, value.Typeface.Style);
+                SetCurrentValue(FontWeightProperty, value.Typeface.Weight);
+                SetCurrentValue(FontStretchProperty, value.Typeface.Stretch);
+            }
+        }
+
+        private GenericTextRunProperties CreateTextRunProperties()
+        {
+            Typeface typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
+            return new GenericTextRunProperties(typeface, FontFeatures, FontSize,
+                textDecorations: null,
+                foregroundBrush: Brushes.Black,
+                backgroundBrush: null,
+                baselineAlignment: BaselineAlignment.Baseline,
+                cultureInfo: null);
+        }
+
+        // TextParagraphProperties
+
+        private GenericTextParagraphProperties? _textParagraphProperties;
+        public GenericTextParagraphProperties TextParagraphProperties
+        {
+            get
+            {
+                return _textParagraphProperties ??= CreateTextParagraphProperties();
+            }
+            set
+            {
+                if (value == null)
+                    throw new ArgumentNullException(nameof(value));
+
+                _textParagraphProperties = null;
+                SetCurrentValue(FlowDirectionProperty, value.FlowDirection);
+            }
+        }
+
+        private GenericTextParagraphProperties CreateTextParagraphProperties()
+        {
+            return new GenericTextParagraphProperties(
+                FlowDirection,
+                TextAlignment.Start,
+                firstLineInParagraph: false,
+                alwaysCollapsible: false,
+                TextRunProperties,
+                textWrapping: TextWrapping.NoWrap,
+                lineHeight: 0,
+                indent: 0,
+                letterSpacing: 0);
+        }
+
+        private readonly ITextSource _textSource;
+        private class TextSource : ITextSource
+        {
+            private readonly InteractiveLineControl _owner;
+
+            public TextSource(InteractiveLineControl owner)
+            {
+                _owner = owner;
+            }
+
+            public TextRun? GetTextRun(int textSourceIndex)
+            {
+                string text = _owner.Text ?? string.Empty;
+
+                if (textSourceIndex < 0 || textSourceIndex >= text.Length)
+                    return null;
+
+                return new TextCharacters(text, _owner.TextRunProperties);
+            }
+        }
+
+        private TextLine? _textLine;
+        public TextLine? TextLine => _textLine ??= TextFormatter.Current.FormatLine(_textSource, 0, Bounds.Size.Width, TextParagraphProperties);
+
+        private TextLayout? _textLayout;
+        public TextLayout TextLayout => _textLayout ??= new TextLayout(_textSource, TextParagraphProperties);
+
+        private Size? _textLineSize;
+        protected Size TextLineSize => _textLineSize ??= TextLine is { } textLine ? new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height) : default;
+
+        private Size? _inkSize;
+        protected Size InkSize => _inkSize ??= TextLine is { } textLine ? new Size(textLine.OverhangLeading + textLine.WidthIncludingTrailingWhitespace + textLine.OverhangTrailing, textLine.Extent) : default;
+
+        public event EventHandler? TextLineChanged;
+
+        public InteractiveLineControl()
+        {
+            _textSource = new TextSource(this);
+
+            RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
+            RenderOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias);
+        }
+
+        private void InvalidateTextRunProperties()
+        {
+            _textRunProperties = null;
+            InvalidateTextParagraphProperties();
+        }
+
+        private void InvalidateTextParagraphProperties()
+        {
+            _textParagraphProperties = null;
+            InvalidateTextLine();
+        }
+
+        private void InvalidateTextLine()
+        {
+            _textLayout = null;
+            _textLine = null;
+            _textLineSize = null;
+            _inkSize = null;
+            InvalidateMeasure();
+            InvalidateVisual();
+            
+            TextLineChanged?.Invoke(this, EventArgs.Empty);
+        }
+
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            switch (change.Property.Name)
+            {
+                case nameof(FontFamily):
+                case nameof(FontSize):
+                    InvalidateTextRunProperties();
+                    break;
+
+                case nameof(FontStyle):
+                case nameof(FontWeight):
+                case nameof(FontStretch):
+                    InvalidateTextRunProperties();
+                    break;
+
+                case nameof(FlowDirection):
+                    InvalidateTextParagraphProperties();
+                    break;
+
+                case nameof(Text):
+                    InvalidateTextLine();
+                    break;
+
+                case nameof(BaselineStroke):
+                    _baselinePen = null;
+                    InvalidateVisual();
+                    break;
+
+                case nameof(TextBoundsStroke):
+                    _textBoundsPen = null;
+                    InvalidateVisual();
+                    break;
+
+                case nameof(RunBoundsStroke):
+                    _runBoundsPen = null;
+                    InvalidateVisual();
+                    break;
+
+                case nameof(NextHitStroke):
+                    _nextHitPen = null;
+                    InvalidateVisual();
+                    break;
+
+                case nameof(PreviousHitStroke):
+                    _previousHitPen = null;
+                    InvalidateVisual();
+                    break;
+
+                case nameof(BackspaceHitStroke):
+                    _backspaceHitPen = null;
+                    InvalidateVisual();
+                    break;
+            }
+
+            base.OnPropertyChanged(change);
+        }
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (TextLine == null)
+                return default;
+
+            return new Size(Math.Max(TextLineSize.Width, InkSize.Width), Math.Max(TextLineSize.Height, InkSize.Height));
+        }
+
+        private const double VerticalSpacing = 5;
+        private const double HorizontalSpacing = 5;
+        private const double ArrowSize = 5;
+
+        private Dictionary<string, FormattedText> _labelsCache = new();
+        protected FormattedText GetOrCreateLabel(string label, IBrush brush, bool disableCache = false)
+        {
+            if (_labelsCache.TryGetValue(label, out var text))
+                return text;
+
+            text = new FormattedText(label, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, Typeface.Default, 8, brush);
+
+            if (!disableCache)
+                _labelsCache[label] = text;
+
+            return text;
+        }
+
+        private Rect _inkRenderBounds;
+        private Rect _lineRenderBounds;
+
+        public Rect InkRenderBounds => _inkRenderBounds;
+        public Rect LineRenderBounds => _lineRenderBounds;
+
+        public override void Render(DrawingContext context)
+        {
+            TextLine? textLine = TextLine;
+            if (textLine == null)
+                return;
+
+            // overhang leading should be negative when extending (e.g. for j)   WPF: "When the leading alignment point comes before the leading drawn pixel, the value is negative." - docs wrong but values correct
+            // overhang trailing should be negative when extending (e.g. for f)  WPF: "The OverhangTrailing value will be positive when the trailing drawn pixel comes before the trailing alignment point."
+            // overhang after should be negative when inside (e.g. for x) WPF: "The value is positive if the bottommost drawn pixel goes below the line bottom, and is negative if it is within (on or above) the line."
+            // => we want overhang before to be negative when inside (e.g. for x) 
+
+            double overhangBefore = textLine.Extent - textLine.OverhangAfter - textLine.Height;
+            Rect inkBounds = new Rect(new Point(textLine.OverhangLeading, -overhangBefore), InkSize);
+            Rect lineBounds = new Rect(new Point(0, 0), TextLineSize);
+
+            if (inkBounds.Left < 0)
+                lineBounds = lineBounds.Translate(new Vector(-inkBounds.Left, 0));
+
+            if (inkBounds.Top < 0)
+                lineBounds = lineBounds.Translate(new Vector(0, -inkBounds.Top));
+
+            _inkRenderBounds = inkBounds;
+            _lineRenderBounds = lineBounds;
+
+            Rect bounds = new Rect(0, 0, Math.Max(inkBounds.Right, lineBounds.Right), Math.Max(inkBounds.Bottom, lineBounds.Bottom));
+            double labelX = bounds.Right + HorizontalSpacing;
+
+            if (Background is IBrush background)
+                context.FillRectangle(background, lineBounds);
+
+            if (ExtentStroke != null)
+            {
+                context.DrawRectangle(ExtentPen, inkBounds);
+                RenderLabel(context, nameof(textLine.Extent), ExtentStroke, labelX, inkBounds.Top);
+            }
+
+            using (context.PushTransform(Matrix.CreateTranslation(lineBounds.Left, lineBounds.Top)))
+            {
+                labelX -= lineBounds.Left; // labels to ignore horizontal transform
+
+                if (BaselineStroke != null)
+                {
+                    RenderFontLine(context, textLine.Baseline, lineBounds.Width, BaselinePen); // no other lines currently available in Avalonia
+                    RenderLabel(context, nameof(textLine.Baseline), BaselineStroke, labelX, textLine.Baseline);
+                }
+
+                textLine.Draw(context, lineOrigin: default);
+
+                var runBoundsStroke = RunBoundsStroke;
+                if (TextBoundsStroke != null || runBoundsStroke != null)
+                {
+                    IReadOnlyList<TextBounds> textBounds = textLine.GetTextBounds(textLine.FirstTextSourceIndex, textLine.Length);
+                    foreach (var textBound in textBounds)
+                    {
+                        if (runBoundsStroke != null)
+                        {
+                            var runBounds = textBound.TextRunBounds;
+                            foreach (var runBound in runBounds)
+                                context.DrawRectangle(RunBoundsPen, runBound.Rectangle);
+                        }
+
+                        context.DrawRectangle(TextBoundsPen, textBound.Rectangle);
+                    }
+                }
+
+                double y = inkBounds.Bottom - lineBounds.Top + VerticalSpacing * 2;
+
+                if (NextHitStroke != null)
+                {
+                    RenderHits(context, NextHitPen, textLine, textLine.GetNextCaretCharacterHit, new CharacterHit(0), ref y);
+                    RenderLabel(context, nameof(textLine.GetNextCaretCharacterHit), NextHitStroke, labelX, y);
+                    y += VerticalSpacing * 2;
+                }
+
+                if (PreviousHitStroke != null)
+                {
+                    RenderLabel(context, nameof(textLine.GetPreviousCaretCharacterHit), PreviousHitStroke, labelX, y);
+                    RenderHits(context, PreviousHitPen, textLine, textLine.GetPreviousCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
+                    y += VerticalSpacing * 2;
+                }
+
+                if (BackspaceHitStroke != null)
+                {
+                    RenderLabel(context, nameof(textLine.GetBackspaceCaretCharacterHit), BackspaceHitStroke, labelX, y);
+                    RenderHits(context, BackspaceHitPen, textLine, textLine.GetBackspaceCaretCharacterHit, new CharacterHit(textLine.Length), ref y);
+                    y += VerticalSpacing * 2;
+                }
+
+                if (DistanceStroke != null)
+                {
+                    y += VerticalSpacing;
+
+                    var label = RenderLabel(context, nameof(textLine.GetDistanceFromCharacterHit), DistanceStroke, 0, y);
+                    y += label.Height;
+
+                    for (int i = 0; i < textLine.Length; i++)
+                    {
+                        var hit = new CharacterHit(i);
+                        CharacterHit prevHit = default, nextHit = default;
+
+                        double leftLabelX = -HorizontalSpacing;
+
+                        // we want z-order to be previous, next, distance
+                        // but labels need to be ordered next, distance, previous
+                        if (NextHitStroke != null)
+                        {
+                            nextHit = textLine.GetNextCaretCharacterHit(hit);
+                            var nextLabel = RenderLabel(context, $" > {nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}", NextHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
+                            leftLabelX -= nextLabel.WidthIncludingTrailingWhitespace;
+                        }
+
+                        if (PreviousHitStroke != null)
+                        {
+                            prevHit = textLine.GetPreviousCaretCharacterHit(hit);
+                            var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex, 0));
+                            var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(prevHit.FirstCharacterIndex + prevHit.TrailingLength, 0));
+                            RenderHorizontalPoint(context, x1, x2, y, PreviousHitPen, ArrowSize);
+                        }
+
+                        if (NextHitStroke != null)
+                        {
+                            var x1 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex, 0));
+                            var x2 = textLine.GetDistanceFromCharacterHit(new CharacterHit(nextHit.FirstCharacterIndex + nextHit.TrailingLength, 0));
+                            RenderHorizontalPoint(context, x1, x2, y, NextHitPen, ArrowSize);
+                        }
+
+                        label = RenderLabel(context, $"[{i}]", DistanceStroke, leftLabelX, y, TextAlignment.Right);
+                        leftLabelX -= label.WidthIncludingTrailingWhitespace;
+
+                        if (PreviousHitStroke != null)
+                            RenderLabel(context, $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength} < ", PreviousHitStroke, leftLabelX, y, TextAlignment.Right, disableCache: true);
+
+                        double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(i));
+                        RenderHorizontalBar(context, 0, distance, y, DistancePen, ArrowSize);
+                        //RenderLabel(context, distance.ToString("F2"), DistanceStroke, distance + HorizontalSpacing, y, disableCache: true);
+
+                        y += label.Height;
+                    }
+                }
+            }
+        }
+
+        [return: NotNullIfNotNull("brush")]
+        private FormattedText? RenderLabel(DrawingContext context, string label, IBrush? brush, double x, double y, TextAlignment alignment = TextAlignment.Left, bool disableCache = false)
+        {
+            if (brush == null)
+                return null;
+
+            var text = GetOrCreateLabel(label, brush, disableCache);
+            
+            if (alignment == TextAlignment.Right)
+                context.DrawText(text, new Point(x - text.WidthIncludingTrailingWhitespace, y - text.Height / 2));
+            else
+                context.DrawText(text, new Point(x, y - text.Height / 2));
+
+            return text;
+        }
+
+        private void RenderHits(DrawingContext context, IPen hitPen, TextLine textLine, Func<CharacterHit, CharacterHit> nextHit, CharacterHit startingHit, ref double y)
+        {
+            CharacterHit lastHit = startingHit;
+            double lastX = textLine.GetDistanceFromCharacterHit(lastHit);
+            double lastDirection = 0;
+            y -= VerticalSpacing; // we always start with adding one below
+
+            while (true)
+            {
+                CharacterHit hit = nextHit(lastHit);
+                if (hit == lastHit)
+                    break;
+
+                double x = textLine.GetDistanceFromCharacterHit(hit);
+                double direction = Math.Sign(x - lastX);
+
+                if (direction == 0 || lastDirection != direction)
+                    y += VerticalSpacing;
+
+                if (direction == 0)
+                    RenderPoint(context, x, y, hitPen, ArrowSize);
+                else
+                    RenderHorizontalArrow(context, lastX, x, y, hitPen, ArrowSize);
+                 
+                lastX = x;
+                lastHit = hit;
+                lastDirection = direction;
+            }
+        }
+
+        private void RenderPoint(DrawingContext context, double x, double y, IPen pen, double arrowHeight)
+        {
+            context.DrawEllipse(pen.Brush, pen, new Point(x, y), ArrowSize / 2, ArrowSize / 2);
+        }
+
+        private void RenderHorizontalPoint(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
+        {
+            PathGeometry startCap = new PathGeometry();
+            PathFigure startFigure = new PathFigure();
+            startFigure.StartPoint = new Point(xStart, y - size / 2);
+            startFigure.IsClosed = true;
+            startFigure.IsFilled = true;
+            startFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xStart, y + size / 2), SweepDirection = SweepDirection.CounterClockwise });
+            startCap.Figures!.Add(startFigure);
+
+            context.DrawGeometry(pen.Brush, pen, startCap);
+
+            PathGeometry endCap = new PathGeometry();
+            PathFigure endFigure = new PathFigure();
+            endFigure.StartPoint = new Point(xEnd, y - size / 2);
+            endFigure.IsClosed = true;
+            endFigure.IsFilled = false;
+            endFigure.Segments!.Add(new ArcSegment { Size = new Size(size / 2, size / 2), Point = new Point(xEnd, y + size / 2), SweepDirection = SweepDirection.Clockwise });
+            endCap.Figures!.Add(endFigure);
+
+            context.DrawGeometry(pen.Brush, pen, endCap);
+        }
+
+        private void RenderHorizontalArrow(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
+        {
+            context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
+            context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
+
+            if (xEnd >= xStart)
+                context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
+                [
+                    new Point(xEnd - size, y - size / 2),
+                    new Point(xEnd - size, y + size/2),
+                    new Point(xEnd, y)
+                ], isFilled: true));
+            else
+                context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
+                [
+                    new Point(xEnd + size, y - size / 2),
+                    new Point(xEnd + size, y + size/2),
+                    new Point(xEnd, y)
+                ], isFilled: true));
+        }
+        private void RenderHorizontalBar(DrawingContext context, double xStart, double xEnd, double y, IPen pen, double size)
+        {
+            context.DrawLine(pen, new Point(xStart, y), new Point(xEnd, y));
+            context.DrawLine(pen, new Point(xStart, y - size / 2), new Point(xStart, y + size / 2)); // start cap
+            context.DrawLine(pen, new Point(xEnd, y - size / 2), new Point(xEnd, y + size / 2)); // end cap
+        }
+
+        private void RenderFontLine(DrawingContext context, double y, double width, IPen pen)
+        {
+            context.DrawLine(pen, new Point(0, y), new Point(width, y));
+        }
+    }
+}

+ 105 - 0
samples/TextTestApp/MainWindow.axaml

@@ -0,0 +1,105 @@
+<Window xmlns="https://github.com/avaloniaui"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:local="clr-namespace:TextTestApp"
+    x:Class="TextTestApp.MainWindow"
+    Title="Text Test App" Width="700" Height="700">
+
+    <DockPanel>
+      <Border DockPanel.Dock="Bottom" Background="WhiteSmoke" BorderThickness="0,1,0,0" BorderBrush="Silver" Padding="2">
+        <DockPanel>
+          <ToggleSwitch Name="_hitRangeToggle" DockPanel.Dock="Right" OnContent="HitTestTextRange" OffContent="HitTestTextPosition" IsCheckedChanged="OnHitTestMethodChanged" />
+          <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
+            <TextBlock Text="HitTestPoint:" Margin="5,0" />
+            <TextBlock Name="_coordinates" MinWidth="120" />
+            <Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" />
+            <TextBlock Text="TextPosition:" Margin="5,0" />
+            <TextBlock Name="_hit" MinWidth="60" />
+            <Border Width="5" BorderThickness="1,0,0,0" BorderBrush="Silver" UseLayoutRounding="True" Margin="5,0,0,0" />
+          </StackPanel>
+        </DockPanel>
+      </Border>
+
+      <DockPanel DockPanel.Dock="Top" Margin="5">
+        <StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
+          <Label Content="_Font:" Target="{Binding ElementName=_font}" VerticalAlignment="Center" Margin="5,0,0,0" />
+          <ComboBox Name="_font" ItemsSource="{Binding SystemFonts, Source={x:Static FontManager.Current}}" />
+          <Label Content="_Size:" Target="{Binding ElementName=_size}" VerticalAlignment="Center" Margin="5,0,0,0" />
+          <TextBox Name="_size" VerticalAlignment="Center" Text="64" />
+          <Button VerticalAlignment="Center" Click="OnNewWindowClick" ToolTip.Tip="New window" Margin="5,0,0,0">+</Button>
+        </StackPanel>
+
+        <Label Content="_Text:" Target="{Binding ElementName=_text}"  VerticalAlignment="Center"/>
+        <TextBox Name="_text" Text="Hello!" VerticalAlignment="Center" />
+      </DockPanel>
+
+      <Grid RowDefinitions="*,5,*">
+        <local:InteractiveLineControl Name="_rendering" DockPanel.Dock="Top" Margin="16" HorizontalAlignment="Center" 
+                                  
+                                      Text="{Binding Text, ElementName=_text}"
+                                      FontFamily="{Binding SelectedValue, ElementName=_font}"
+                                      FontSize="{Binding Text, ElementName=_size}"
+                                      Background="BlanchedAlmond"
+                                      ExtentStroke="Black"
+                                      BaselineStroke="Blue"
+                                      TextBoundsStroke="Goldenrod"
+                                      RunBoundsStroke="Gold"
+                                      NextHitStroke="Green"
+                                      PreviousHitStroke="Blue"
+                                      BackspaceHitStroke="Red"
+                                      DistanceStroke="Black"
+                                  
+                                      PointerMoved="OnPointerMoved"
+          />
+        
+        <GridSplitter Grid.Row="1" />
+
+        <TabControl Grid.Row="2" DockPanel.Dock="Bottom" Background="White" BorderBrush="Whitesmoke" BorderThickness="0,1,0,0">
+          <TabItem Header="Shaped Buffer">
+            <ListBox Name="_buffer" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionMode="Multiple" SelectionChanged="OnBufferSelectionChanged" Background="Transparent">
+              <ListBox.Styles>
+                <Style Selector="ListBoxItem">
+                  <Setter Property="Padding" Value="0"/>
+                  <Setter Property="Background" Value="White" />
+                </Style>
+              </ListBox.Styles>
+              <Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1">
+                <local:GridRow ColumnSpacing="10">
+                  <TextBlock Text="" />
+                  <TextBlock Text="Index" />
+                  <TextBlock Text="Characters" />
+                  <TextBlock Text="Codepoints" />
+                  <TextBlock Text="Glyph" />
+                  <TextBlock Text="Glyph ID" />
+                  <TextBlock Text="Advance" />
+                  <TextBlock Text="Offset" />
+                  <TextBlock Text="Ink Bounds" />
+                </local:GridRow>
+              </Border>
+            </ListBox>
+          </TabItem>
+          <TabItem Header="Character Hits">
+            <ListBox Name="_hits" Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Auto" SelectionChanged="OnHitsSelectionChanged" Background="Transparent">
+              <ListBox.Styles>
+                <Style Selector="ListBoxItem">
+                  <Setter Property="Padding" Value="0"/>
+                  <Setter Property="Background" Value="White" />
+                </Style>
+              </ListBox.Styles>
+              <Border Background="WhiteSmoke" BorderBrush="Silver" BorderThickness="0,1">
+                <local:GridRow ColumnSpacing="10">
+                  <TextBlock Text="" />
+                  <TextBlock Text="Backspace Hit" />
+                  <TextBlock Text="Previous Hit" />
+                  <TextBlock Text="Index" />
+                  <TextBlock Text="Next Hit" />
+                  <TextBlock Text="Codepoint" />
+                  <TextBlock Text="Character" />
+                  <TextBlock Text="Distance" />
+                </local:GridRow>
+              </Border>
+            </ListBox>
+          </TabItem>
+        </TabControl>
+      </Grid>
+    </DockPanel>
+</Window>

+ 340 - 0
samples/TextTestApp/MainWindow.axaml.cs

@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+
+namespace TextTestApp
+{
+    public partial class MainWindow : Window
+    {
+        private SelectionAdorner? _selectionAdorner;
+
+        public MainWindow()
+        {
+            InitializeComponent();
+
+            _selectionAdorner = new();
+            _selectionAdorner.Stroke = Brushes.Red;
+            _selectionAdorner.Fill = new SolidColorBrush(Colors.LightSkyBlue, 0.25);
+            _selectionAdorner.IsHitTestVisible = false;
+            AdornerLayer.SetIsClipEnabled(_selectionAdorner, false);
+            AdornerLayer.SetAdorner(_rendering, _selectionAdorner);
+
+            _rendering.TextLineChanged += OnShapeBufferChanged;
+            OnShapeBufferChanged();
+        }
+
+        private void OnNewWindowClick(object? sender, RoutedEventArgs e)
+        {
+            MainWindow win = new MainWindow();
+            win.Show();
+        }
+
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.F5)
+            {
+                _rendering.InvalidateVisual();
+                OnShapeBufferChanged();
+                e.Handled = true;
+            }
+            else if (e.Key == Key.Escape)
+            {
+                if (_hits.IsKeyboardFocusWithin && _hits.SelectedIndex != -1)
+                {
+                    _hits.SelectedIndex = -1;
+                    e.Handled = true;
+                }
+                else if (_buffer.IsKeyboardFocusWithin && _buffer.SelectedIndex != -1)
+                {
+                    _buffer.SelectedIndex = -1;
+                    e.Handled = true;
+                }
+            }
+
+            base.OnKeyDown(e);
+        }
+
+        private void OnShapeBufferChanged(object? sender, EventArgs e) => OnShapeBufferChanged();
+        private void OnShapeBufferChanged()
+        {
+            if (_selectionAdorner == null)
+                return;
+
+            ListBuffers();
+            ListHits();
+
+            Rect bounds = _rendering.LineRenderBounds;
+            _selectionAdorner!.Transform = Matrix.CreateTranslation(bounds.X, bounds.Y);
+        }
+
+        private void ListBuffers()
+        {
+            for (int i = _buffer.ItemCount - 1; i >= 1; i--)
+                _buffer.Items.RemoveAt(i);
+
+            TextLine? textLine = _rendering.TextLine;
+            if (textLine == null)
+                return;
+
+            double currentX = _rendering.LineRenderBounds.Left;
+            foreach (TextRun run in textLine.TextRuns)
+            {
+                if (run is ShapedTextRun shapedRun)
+                {
+                    _buffer.Items.Add(new TextBlock
+                    {
+                        Text = $"{run.GetType().Name}: Bidi = {shapedRun.BidiLevel}, Font = {shapedRun.ShapedBuffer.GlyphTypeface.FamilyName}",
+                        FontWeight = FontWeight.Bold,
+                        Padding = new Thickness(10, 0),
+                        Tag = run,
+                    });
+
+                    ListBuffer(textLine, shapedRun, ref currentX);
+                }
+                else
+                    _buffer.Items.Add(new TextBlock
+                    {
+                        Text = run.GetType().Name,
+                        FontWeight = FontWeight.Bold,
+                        Padding = new Thickness(10, 0),
+                        Tag = run
+                    });
+            }
+        }
+
+        private void ListHits()
+        {
+            for (int i = _hits.ItemCount - 1; i >= 1; i--)
+                _hits.Items.RemoveAt(i);
+
+            TextLine? textLine = _rendering.TextLine;
+            if (textLine == null)
+                return;
+
+            for (int i = 0; i < textLine.Length; i++)
+            {
+                string? clusterText = _rendering.Text!.Substring(i, 1);
+                string? clusterHex = ToHex(clusterText);
+
+                var hit = new CharacterHit(i);
+                var prevHit = textLine.GetPreviousCaretCharacterHit(hit);
+                var nextHit = textLine.GetNextCaretCharacterHit(hit);
+                var bkspHit = textLine.GetBackspaceCaretCharacterHit(hit);
+
+                GridRow row = new GridRow { ColumnSpacing = 10 };
+                row.Children.Add(new Control());
+                row.Children.Add(new TextBlock { Text = $"{bkspHit.FirstCharacterIndex}+{bkspHit.TrailingLength}" });
+                row.Children.Add(new TextBlock { Text = $"{prevHit.FirstCharacterIndex}+{prevHit.TrailingLength}" });
+                row.Children.Add(new TextBlock { Text = i.ToString(), FontWeight = FontWeight.Bold });
+                row.Children.Add(new TextBlock { Text = $"{nextHit.FirstCharacterIndex}+{nextHit.TrailingLength}" });
+                row.Children.Add(new TextBlock { Text = clusterHex });
+                row.Children.Add(new TextBlock { Text = clusterText });
+                row.Children.Add(new TextBlock { Text = textLine.GetDistanceFromCharacterHit(hit).ToString() });
+                row.Tag = i;
+
+                _hits.Items.Add(row);
+            }
+        }
+
+        private static readonly IBrush TransparentAliceBlue = new SolidColorBrush(0x0F0188FF);
+        private static readonly IBrush TransparentAntiqueWhite = new SolidColorBrush(0x28DF8000);
+        private void ListBuffer(TextLine textLine, ShapedTextRun shapedRun, ref double currentX)
+        {
+            ShapedBuffer buffer = shapedRun.ShapedBuffer;
+
+            int lastClusterStart = -1;
+            bool oddCluster = false;
+
+            IReadOnlyList<GlyphInfo> glyphInfos = buffer;
+
+            currentX += shapedRun.GlyphRun.BaselineOrigin.X;
+            for (var i = 0; i < glyphInfos.Count; i++)
+            {
+                GlyphInfo info = glyphInfos[i];
+                int clusterStart = info.GlyphCluster;
+                int clusterLength = FindClusterLenghtAt(i);
+                string? clusterText = _rendering.Text!.Substring(clusterStart, clusterLength);
+                string? clusterHex = ToHex(clusterText);
+
+                Border border = new Border();
+                if (clusterStart == lastClusterStart)
+                {
+                    clusterText = clusterHex = null;
+                }
+                else
+                {
+                    oddCluster = !oddCluster;
+                    lastClusterStart = clusterStart;
+                }
+                border.Background = oddCluster ? TransparentAliceBlue : TransparentAntiqueWhite;
+
+
+                GridRow row = new GridRow { ColumnSpacing = 10 };
+                row.Children.Add(new Control());
+                row.Children.Add(new TextBlock { Text = clusterStart.ToString() });
+                row.Children.Add(new TextBlock { Text = clusterText });
+                row.Children.Add(new TextBlock { Text = clusterHex, TextWrapping = TextWrapping.Wrap });
+                row.Children.Add(new Image { Source = CreateGlyphDrawing(shapedRun.GlyphRun.GlyphTypeface, FontSize, info), Margin = new Thickness(2) });
+                row.Children.Add(new TextBlock { Text = info.GlyphIndex.ToString() });
+                row.Children.Add(new TextBlock { Text = info.GlyphAdvance.ToString() });
+                row.Children.Add(new TextBlock { Text = info.GlyphOffset.ToString() });
+
+                Geometry glyph = GetGlyphOutline(shapedRun.GlyphRun.GlyphTypeface, shapedRun.GlyphRun.FontRenderingEmSize, info);
+                Rect glyphBounds = glyph.Bounds;
+                Rect offsetBounds = glyphBounds.Translate(new Vector(currentX + info.GlyphOffset.X, info.GlyphOffset.Y));
+
+                TextBlock boundsBlock = new TextBlock { Text = offsetBounds.ToString() };
+                ToolTip.SetTip(boundsBlock, "Origin bounds: " + glyphBounds);
+                row.Children.Add(boundsBlock);
+
+                border.Child = row;
+                border.Tag = offsetBounds;
+                _buffer.Items.Add(border);
+                
+                currentX += glyphInfos[i].GlyphAdvance;
+            }
+
+            int FindClusterLenghtAt(int index)
+            {
+                int cluster = glyphInfos[index].GlyphCluster;
+                if (shapedRun.BidiLevel % 2 == 0)
+                {
+                    while (++index < glyphInfos.Count)
+                        if (glyphInfos[index].GlyphCluster != cluster)
+                            return glyphInfos[index].GlyphCluster - cluster;
+
+                    return shapedRun.Length + glyphInfos[0].GlyphCluster - cluster;
+                }
+                else
+                {
+                    while (--index >= 0)
+                        if (glyphInfos[index].GlyphCluster != cluster)
+                            return glyphInfos[index].GlyphCluster - cluster;
+
+                    return shapedRun.Length + glyphInfos[glyphInfos.Count - 1].GlyphCluster - cluster;
+                }
+            }
+        }
+
+        private IImage CreateGlyphDrawing(IGlyphTypeface glyphTypeface, double emSize, GlyphInfo info)
+        {
+            return new DrawingImage { Drawing = new GeometryDrawing { Brush = Brushes.Black, Geometry = GetGlyphOutline(glyphTypeface, emSize, info) } };
+        }
+
+        private Geometry GetGlyphOutline(IGlyphTypeface typeface, double emSize, GlyphInfo info)
+        {
+            // substitute for GlyphTypeface.GetGlyphOutline
+            return new GlyphRun(typeface, emSize, new[] { '\0' }, [info]).BuildGeometry();
+        }
+
+        private void OnPointerMoved(object sender, PointerEventArgs e)
+        {
+            InteractiveLineControl lineControl = (InteractiveLineControl)sender;
+            TextLayout textLayout = lineControl.TextLayout;
+            Rect lineBounds = lineControl.LineRenderBounds;
+
+            PointerPoint pointerPoint = e.GetCurrentPoint(lineControl);
+            Point point = new Point(pointerPoint.Position.X - lineBounds.Left, pointerPoint.Position.Y - lineBounds.Top);
+            _coordinates.Text = $"{pointerPoint.Position.X:F4}, {pointerPoint.Position.Y:F4}";
+
+            TextHitTestResult textHit = textLayout.HitTestPoint(point);
+            _hit.Text = $"{textHit.TextPosition} ({textHit.CharacterHit.FirstCharacterIndex}+{textHit.CharacterHit.TrailingLength})";
+            if (textHit.IsTrailing)
+                _hit.Text += " T";
+
+            if (textHit.IsInside)
+            {
+                _hits.SelectedIndex = textHit.TextPosition + 1; // header
+            }
+            else
+                _hits.SelectedIndex = -1;
+        }
+
+        private void OnHitTestMethodChanged(object? sender, RoutedEventArgs e)
+        {
+            _hits.SelectionMode = _hitRangeToggle.IsChecked == true ? SelectionMode.Multiple : SelectionMode.Single;
+        }
+
+        private void OnHitsSelectionChanged(object? sender, SelectionChangedEventArgs e)
+        {
+            if (_selectionAdorner == null)
+                return;
+
+            List<Rect> rectangles = new List<Rect>();
+            TextLayout textLayout = _rendering.TextLayout;
+
+            if (_hitRangeToggle.IsChecked == true)
+            {
+                // collect continuous selected indices
+                List<(int start, int length)> selections = new(1);
+
+                int[] indices = _hits.Selection.SelectedIndexes.ToArray();
+                Array.Sort(indices);
+
+                int currentIndex = -1;
+                int currentLength = 0;
+                for (int i = 0; i < indices.Length; i++)
+                    if (_hits.Items[indices[i]] is Control { Tag: int index })
+                    {
+                        if (index == currentIndex + currentLength)
+                        {
+                            currentLength++;
+                        }
+                        else
+                        {
+                            if (currentLength > 0)
+                                selections.Add((currentIndex, currentLength));
+
+                            currentIndex = index;
+                            currentLength = 1;
+                        }
+                    }
+
+                if (currentLength > 0)
+                    selections.Add((currentIndex, currentLength));
+
+                foreach (var selection in selections)
+                {
+                    var selectionRectangles = textLayout.HitTestTextRange(selection.start, selection.length);
+                    rectangles.AddRange(selectionRectangles);
+                }
+            }
+            else
+            {
+                if (_hits.SelectedItem is Control { Tag: int index })
+                {
+                    Rect rect = textLayout.HitTestTextPosition(index);
+                    rectangles.Add(rect);
+                }
+            }
+
+            _selectionAdorner.Rectangles = rectangles;
+        }
+
+        private void OnBufferSelectionChanged(object? sender, SelectionChangedEventArgs e)
+        {
+            List<Rect> rectangles = new List<Rect>(_buffer.Selection.Count);
+
+            foreach (var row in _buffer.SelectedItems)
+                if (row is Control { Tag: Rect rect })
+                    rectangles.Add(rect);
+
+            _selectionAdorner.Rectangles = rectangles;
+        }
+
+        private static string ToHex(string s)
+        {
+            if (string.IsNullOrEmpty(s))
+                return s;
+
+            return string.Join(" ", s.Select(c => ((int)c).ToString("X4")));
+        }
+    }
+}

+ 25 - 0
samples/TextTestApp/Program.cs

@@ -0,0 +1,25 @@
+using System;
+using Avalonia;
+
+namespace TextTestApp
+{
+    static class Program
+    {
+        // Initialization code. Don't use any Avalonia, third-party APIs or any
+        // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+        // yet and stuff might break.
+        [STAThread]
+        public static void Main(string[] args)
+        {
+            BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+        }
+
+        // Avalonia configuration, don't remove; also used by visual designer.
+        public static AppBuilder BuildAvaloniaApp()
+        {
+            return AppBuilder.Configure<App>()
+                        .UsePlatformDetect()
+                        .LogToTrace();
+        }
+    }
+}

+ 90 - 0
samples/TextTestApp/SelectionAdorner.cs

@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace TextTestApp
+{
+    public class SelectionAdorner : Control
+    {
+        public static readonly StyledProperty<IBrush?> FillProperty =
+            AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Fill));
+
+        public static readonly StyledProperty<IBrush?> StrokeProperty =
+            AvaloniaProperty.Register<SelectionAdorner, IBrush?>(nameof(Stroke));
+
+        public static readonly StyledProperty<Matrix> TransformProperty =
+            AvaloniaProperty.Register<SelectionAdorner, Matrix>(nameof(Transform), Matrix.Identity);
+
+        public Matrix Transform
+        {
+            get => this.GetValue(TransformProperty);
+            set => SetValue(TransformProperty, value);
+        }
+
+        public IBrush? Stroke
+        {
+            get => GetValue(StrokeProperty);
+            set => SetValue(StrokeProperty, value);
+        }
+
+        public IBrush? Fill
+        {
+            get => GetValue(FillProperty);
+            set => SetValue(FillProperty, value);
+        }
+
+        private IList<Rect>? _rectangles;
+        public IList<Rect>? Rectangles
+        {
+            get => _rectangles;
+            set
+            {
+                _rectangles = value;
+                InvalidateVisual();
+            }
+        }
+
+        public SelectionAdorner()
+        {
+            AffectsRender<SelectionAdorner>(FillProperty, StrokeProperty, TransformProperty);
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            var rectangles = Rectangles;
+            if (rectangles == null)
+                return;
+
+            using (context.PushTransform(Transform))
+            {
+                Pen pen = new Pen(Stroke, 1);
+                for (int i = 0; i < rectangles.Count; i++)
+                {
+                    Rect rectangle = rectangles[i];
+                    Rect normalized = rectangle.Width < 0 ? new Rect(rectangle.TopRight, rectangle.BottomLeft) : rectangle;
+
+                    if (rectangles[i].Width == 0)
+                        context.DrawLine(pen, rectangle.TopLeft, rectangle.BottomRight);
+                    else
+                        context.DrawRectangle(Fill, pen, normalized);
+
+                    RenderCue(context, pen, rectangle.TopLeft, 5, isFilled: true);
+                    RenderCue(context, pen, rectangle.TopRight, 5, isFilled: false);
+                }
+            }
+        }
+
+        private void RenderCue(DrawingContext context, IPen pen, Point p, double size, bool isFilled)
+        {
+            context.DrawGeometry(pen.Brush, pen, new PolylineGeometry(
+            [
+                new Point(p.X - size / 2, p.Y - size),
+                new Point(p.X + size / 2, p.Y - size),
+                new Point(p.X, p.Y),
+                new Point(p.X - size / 2, p.Y - size),
+            ], isFilled));
+        }
+    }
+}

+ 23 - 0
samples/TextTestApp/TextTestApp.csproj

@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
+    <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
+    <ApplicationManifest>app.manifest</ApplicationManifest>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
+  </ItemGroup>
+
+  <Import Project="..\..\build\SampleApp.props" />
+  <Import Project="..\..\build\ReferenceCoreLibraries.props" />
+  <Import Project="..\..\build\BuildTargets.targets" />
+  <Import Project="..\..\build\SourceGenerators.props" />
+
+</Project>

+ 28 - 0
samples/TextTestApp/app.manifest

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <assemblyIdentity version="1.0.0.0" name="ControlCatalog.app"/>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- A list of the Windows versions that this application has been tested on
+           and is designed to work with. Uncomment the appropriate elements
+           and Windows will automatically select the most compatible environment. -->
+
+      <!-- Windows Vista -->
+      <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
+
+      <!-- Windows 7 -->
+      <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
+
+      <!-- Windows 8 -->
+      <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
+
+      <!-- Windows 8.1 -->
+      <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
+
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+
+    </application>
+  </compatibility>
+</assembly>

+ 4 - 0
src/Avalonia.Base/Media/CharacterHit.cs

@@ -35,6 +35,10 @@ namespace Avalonia.Media
         /// <summary>
         ///     Gets the trailing length value for the character that got hit.
         /// </summary>
+        /// <remarks>
+        /// In the case of a leading edge, this value is 0. In the case of a trailing edge,
+        /// this value is the number of code points until the next valid caret position.
+        /// </remarks>
         public int TrailingLength { get; }
 
         public bool Equals(CharacterHit other)

+ 32 - 53
src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs

@@ -1,7 +1,7 @@
 namespace Avalonia.Media.TextFormatting
 {
     /// <summary>
-    /// Generic implementation of TextParagraphProperties
+    /// Generic implementation of <see cref="TextParagraphProperties"/>.
     /// </summary>
     public sealed class GenericTextParagraphProperties : TextParagraphProperties
     {
@@ -11,45 +11,45 @@
         private double _lineHeight;
 
         /// <summary>
-        /// Constructing TextParagraphProperties
+        /// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/>.
         /// </summary>
-        /// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
-        /// <param name="textAlignment">logical horizontal alignment</param>
-        /// <param name="textWrap">text wrap option</param>
-        /// <param name="lineHeight">Paragraph line height</param>
-        /// <param name="letterSpacing">letter spacing</param>
+        /// <param name="defaultTextRunProperties">Default text run properties, such as typeface or foreground brush.</param>
+        /// <param name="textAlignment">The alignment of inline content in a block.</param>
+        /// <param name="textWrapping">A value that controls whether text wraps when it reaches the flow edge of its containing block box.</param>
+        /// <param name="lineHeight">Paragraph's line spacing.</param>
+        /// <param name="letterSpacing">The amount of letter spacing.</param>
         public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
             TextAlignment textAlignment = TextAlignment.Left,
-            TextWrapping textWrap = TextWrapping.NoWrap,
+            TextWrapping textWrapping = TextWrapping.NoWrap,
             double lineHeight = 0,
             double letterSpacing = 0)
         {
             DefaultTextRunProperties = defaultTextRunProperties;
             _textAlignment = textAlignment;
-            _textWrap = textWrap;
+            _textWrap = textWrapping;
             _lineHeight = lineHeight;
             LetterSpacing = letterSpacing;
         }
 
         /// <summary>
-        /// Constructing TextParagraphProperties
+        /// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/>.
         /// </summary>
-        /// <param name="flowDirection">text flow direction</param>
-        /// <param name="textAlignment">logical horizontal alignment</param>
-        /// <param name="firstLineInParagraph">true if the paragraph is the first line in the paragraph</param>
-        /// <param name="alwaysCollapsible">true if the line is always collapsible</param>
-        /// <param name="defaultTextRunProperties">default paragraph's default run properties</param>
-        /// <param name="textWrap">text wrap option</param>
-        /// <param name="lineHeight">Paragraph line height</param>
-        /// <param name="indent">line indentation</param>
-        /// <param name="letterSpacing">letter spacing</param>
+        /// <param name="flowDirection">The primary text advance direction.</param>
+        /// <param name="textAlignment">The alignment of inline content in a block.</param>
+        /// <param name="firstLineInParagraph"><see langword="true"/> if the paragraph is the first line in the paragraph</param>
+        /// <param name="alwaysCollapsible"><see langword="true"/> if the formatted line may always be collapsed. If <see langword="false"/> (the default), only lines that overflow the paragraph width are collapsed.</param>
+        /// <param name="defaultTextRunProperties">Default text run properties, such as typeface or foreground brush.</param>
+        /// <param name="textWrapping">A value that controls whether text wraps when it reaches the flow edge of its containing block box.</param>
+        /// <param name="lineHeight">Paragraph's line spacing.</param>
+        /// <param name="indent">The amount of line indentation.</param>
+        /// <param name="letterSpacing">The amount of letter spacing.</param>
         public GenericTextParagraphProperties(
             FlowDirection flowDirection,
             TextAlignment textAlignment,
             bool firstLineInParagraph,
             bool alwaysCollapsible,
             TextRunProperties defaultTextRunProperties,
-            TextWrapping textWrap,
+            TextWrapping textWrapping,
             double lineHeight,
             double indent,
             double letterSpacing)
@@ -59,16 +59,16 @@
             FirstLineInParagraph = firstLineInParagraph;
             AlwaysCollapsible = alwaysCollapsible;
             DefaultTextRunProperties = defaultTextRunProperties;
-            _textWrap = textWrap;
+            _textWrap = textWrapping;
             _lineHeight = lineHeight;
             LetterSpacing = letterSpacing;
             Indent = indent;
         }
 
         /// <summary>
-        /// Constructing TextParagraphProperties from another one
+        /// Initializes a new instance of the <see cref="GenericTextParagraphProperties"/> with values copied from the specified <see cref="TextParagraphProperties"/>.
         /// </summary>
-        /// <param name="textParagraphProperties">source line props</param>
+        /// <param name="textParagraphProperties">The <see cref="TextParagraphProperties"/> to copy values from.</param>
         public GenericTextParagraphProperties(TextParagraphProperties textParagraphProperties)
             : this(textParagraphProperties.FlowDirection, 
                 textParagraphProperties.TextAlignment,
@@ -82,64 +82,43 @@
         {
         }
 
-        /// <summary>
-        /// This property specifies whether the primary text advance
-        /// direction shall be left-to-right, right-to-left, or top-to-bottom.
-        /// </summary>
+        /// <inheritdoc/>
         public override FlowDirection FlowDirection
         {
             get { return _flowDirection; }
         }
 
-        /// <summary>
-        /// This property describes how inline content of a block is aligned.
-        /// </summary>
+        /// <inheritdoc/>
         public override TextAlignment TextAlignment
         {
             get { return _textAlignment; }
         }
 
-        /// <summary>
-        /// Paragraph's line height
-        /// </summary>
+        /// <inheritdoc/>
         public override double LineHeight
         {
             get { return _lineHeight; }
         }
 
-        /// <summary>
-        /// Indicates the first line of the paragraph.
-        /// </summary>
+        /// <inheritdoc/>
         public override bool FirstLineInParagraph { get; }
 
-        /// <summary>
-        /// If true, the formatted line may always be collapsed. If false (the default),
-        /// only lines that overflow the paragraph width are collapsed.
-        /// </summary>
+        /// <inheritdoc/>
         public override bool AlwaysCollapsible { get; }
 
-        /// <summary>
-        /// Paragraph's default run properties
-        /// </summary>
+        /// <inheritdoc/>
         public override TextRunProperties DefaultTextRunProperties { get; }
 
-        /// <summary>
-        /// This property controls whether or not text wraps when it reaches the flow edge
-        /// of its containing block box
-        /// </summary>
+        /// <inheritdoc/>
         public override TextWrapping TextWrapping
         {
             get { return _textWrap; }
         }
 
-        /// <summary>
-        /// Line indentation
-        /// </summary>
+        /// <inheritdoc/>
         public override double Indent { get; }
 
-        /// <summary>
-        /// The letter spacing
-        /// </summary>
+        /// <inheritdoc/>
         public override double LetterSpacing { get; }
 
         /// <summary>

+ 17 - 12
src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs

@@ -6,63 +6,68 @@
     public abstract class TextParagraphProperties
     {
         /// <summary>
-        /// This property specifies whether the primary text advance 
-        /// direction shall be left-to-right, right-to-left.
+        /// Gets a value that specifies whether the primary text advance direction shall be left-to-right, or right-to-left.
         /// </summary>
         public abstract FlowDirection FlowDirection { get; }
 
         /// <summary>
-        /// Gets the text alignment.
+        /// Gets a value that describes how an inline content of a block is aligned.
         /// </summary>
         public abstract TextAlignment TextAlignment { get; }
 
         /// <summary>
-        /// Paragraph's line height
+        /// Gets the height of a line of text.
         /// </summary>
         public abstract double LineHeight { get; }
 
         /// <summary>
-        /// Paragraph's line spacing
+        /// Gets or sets paragraph's line spacing.
         /// </summary>
         internal double LineSpacing { get; set; }
 
         /// <summary>
-        /// Indicates the first line of the paragraph.
+        /// Gets a value that indicates whether the text run is the first line of the paragraph.
         /// </summary>
         public abstract bool FirstLineInParagraph { get; }
 
         /// <summary>
+        /// Gets a value that indicates whether a formatted line can always be collapsed.
+        /// </summary>
+        /// <remarks>
         /// If true, the formatted line may always be collapsed. If false (the default),
         /// only lines that overflow the paragraph width are collapsed.
-        /// </summary>
+        /// </remarks>
         public virtual bool AlwaysCollapsible
         {
             get { return false; }
         }
 
         /// <summary>
-        /// Gets the default text style.
+        /// Gets the default text run properties, such as typeface or foreground brush.
         /// </summary>
         public abstract TextRunProperties DefaultTextRunProperties { get; }
 
         /// <summary>
+        /// Gets the collection of TextDecoration objects.
+        /// </summary>
+        /// <remarks>
         /// If not null, text decorations to apply to all runs in the line. This is in addition
         /// to any text decorations specified by the TextRunProperties for individual text runs.
         /// </summary>
         public virtual TextDecorationCollection? TextDecorations => null;
 
         /// <summary>
-        /// Gets the text wrapping.
+        /// Gets a value that controls whether text wraps when it reaches the flow edge of its containing block box.
         /// </summary>
         public abstract TextWrapping TextWrapping { get; }
 
         /// <summary>
-        /// Line indentation
+        /// Gets the amount of line indentation.
         /// </summary>
         public abstract double Indent { get; }
 
         /// <summary>
-        /// Get the paragraph indentation.
+        /// Gets the paragraph indentation.
         /// </summary>
         public virtual double ParagraphIndent
         {
@@ -75,7 +80,7 @@
         public virtual double DefaultIncrementalTab => 0;
 
         /// <summary>
-        /// Gets the letter spacing.
+        /// Gets the amount of letter spacing.
         /// </summary>
         public virtual double LetterSpacing { get; }
     }

+ 12 - 12
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -374,7 +374,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
 
-                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow);
 
                 var textSource = new SingleBufferTextSource("ABCDEFHFFHFJHKHFK", defaultProperties, true);
 
@@ -488,7 +488,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 {
                     var textLine =
                         formatter.FormatLine(textSource, currentPosition, paragraphWidth,
-                            new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
+                            new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap));
 
                     Assert.NotNull(textLine);
 
@@ -544,7 +544,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
 
-                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
 
                 var textSource = new SingleBufferTextSource(text, defaultProperties);
 
@@ -574,7 +574,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 const string text = "012345";
 
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
-                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
                 var textSource = new SingleBufferTextSource(text, defaultProperties);
                 var formatter = new TextFormatterImpl();
 
@@ -632,7 +632,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 {
                     var textLine =
                         formatter.FormatLine(textSource, currentPosition, 300,
-                            new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
+                            new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow));
 
                     Assert.NotNull(textLine);
 
@@ -712,7 +712,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
 
                 var paragraphProperties =
-                    new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+                    new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
 
                 var textSource = new SingleBufferTextSource(text, defaultProperties);
                 var formatter = new TextFormatterImpl();
@@ -742,7 +742,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             using (Start())
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
-                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
 
                 var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
                 var formatter = new TextFormatterImpl();
@@ -773,7 +773,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
 
                 var paragraphProperties =
-                    new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap);
+                    new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.NoWrap);
 
                 var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
 
@@ -878,7 +878,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             using (Start())
             {
                 var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
-                var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
 
                 var text = "Hello World";
 
@@ -975,7 +975,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         {
             var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
             var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
-                textWrap: wrapping);
+                textWrapping: wrapping);
 
             using (Start())
             {
@@ -993,7 +993,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         {
             var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
             var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
-                textWrap: wrapping);
+                textWrapping: wrapping);
             
             using (Start())
             {
@@ -1077,7 +1077,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             using (Start())
             {
                 var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
-                var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrapping: TextWrapping.Wrap);
 
                 var text = "一二三四 TEXT 一二三四五六七八九十零";
 

+ 1 - 1
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -1168,7 +1168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var defaultProperties =
                    new GenericTextRunProperties(Typeface.Default, 72, foregroundBrush: Brushes.Black);
 
-                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
+                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
 
                 var textLayout = new TextLayout(new SingleBufferTextSource("01", defaultProperties, true), paragraphProperties, maxWidth: 36);