1
0
Эх сурвалжийг харах

Merge branch 'master' into refactor/itemcontainergenerator

Steven Kirk 2 жил өмнө
parent
commit
3c59202132
100 өөрчлөгдсөн 1867 нэмэгдсэн , 1065 устгасан
  1. 2 0
      .editorconfig
  2. 3 0
      samples/ControlCatalog/MainView.xaml
  3. 27 0
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  4. 36 0
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  5. 26 0
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  6. 1 1
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  7. 1 1
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  8. 7 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  9. 0 32
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  10. 0 30
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  11. 11 7
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  12. 1 10
      src/Avalonia.Base/Animation/Cue.cs
  13. 1 1
      src/Avalonia.Base/Data/BindingValue.cs
  14. 152 0
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  15. 8 0
      src/Avalonia.Base/Input/Gestures.cs
  16. 5 0
      src/Avalonia.Base/Input/InputElement.cs
  17. 2 2
      src/Avalonia.Base/Input/PointerPoint.cs
  18. 43 0
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  19. 1 1
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  20. 1 1
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  21. 1 1
      src/Avalonia.Base/Logging/ParametrizedLogger.cs
  22. 1 1
      src/Avalonia.Base/Matrix.cs
  23. 6 0
      src/Avalonia.Base/Media/BoxShadow.cs
  24. 8 0
      src/Avalonia.Base/Media/BoxShadows.cs
  25. 1 1
      src/Avalonia.Base/Media/DrawingContext.cs
  26. 1 1
      src/Avalonia.Base/Media/FontMetrics.cs
  27. 2 6
      src/Avalonia.Base/Media/FormattedText.cs
  28. 1 1
      src/Avalonia.Base/Media/GlyphMetrics.cs
  29. 236 183
      src/Avalonia.Base/Media/GlyphRun.cs
  30. 13 7
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  31. 2 0
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  32. 1 1
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  33. 293 0
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  34. 115 0
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  35. 7 6
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  36. 12 7
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  37. 8 12
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  38. 20 13
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  39. 9 10
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  40. 3 1
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  41. 92 37
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  42. 52 50
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  43. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  44. 52 48
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  45. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  46. 2 3
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  47. 177 181
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  48. 4 4
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  49. 3 3
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  50. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextRange.cs
  51. 6 5
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  52. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  53. 7 4
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  54. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  55. 1 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  56. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  57. 2 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  58. 5 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  59. 4 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  60. 4 4
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  61. 6 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  62. 1 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs
  63. 8 7
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  64. 1 1
      src/Avalonia.Base/Media/TextHitTestResult.cs
  65. 3 8
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  66. 3 8
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  67. 1 1
      src/Avalonia.Base/Media/TextTrimming.cs
  68. 6 6
      src/Avalonia.Base/Media/Transformation/TransformOperation.cs
  69. 1 1
      src/Avalonia.Base/Media/Transformation/TransformOperations.cs
  70. 2 2
      src/Avalonia.Base/Media/UnicodeRange.cs
  71. 18 0
      src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs
  72. 11 3
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  73. 20 9
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  74. 5 5
      src/Avalonia.Base/Platform/IRenderTarget.cs
  75. 1 1
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  76. 2 3
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  77. 4 0
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  78. 5 4
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  79. 4 3
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  80. 32 1
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  81. 2 1
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  82. 9 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  83. 48 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  84. 1 1
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  85. 49 4
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  86. 0 6
      src/Avalonia.Base/Rendering/IRenderRoot.cs
  87. 6 0
      src/Avalonia.Base/Rendering/IRenderer.cs
  88. 20 6
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  89. 24 0
      src/Avalonia.Base/Rendering/OwnedDisposable.cs
  90. 66 0
      src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs
  91. 15 11
      src/Avalonia.Base/Rendering/RenderLoop.cs
  92. 2 0
      src/Avalonia.Base/Rendering/SceneGraph/Scene.cs
  93. 1 1
      src/Avalonia.Base/Styling/SelectorMatch.cs
  94. 0 8
      src/Avalonia.Base/Utilities/ArraySlice.cs
  95. 0 239
      src/Avalonia.Base/Utilities/ReadOnlySlice.cs
  96. 1 1
      src/Avalonia.Base/Utilities/SmallDictionary.cs
  97. 1 1
      src/Avalonia.Base/Utilities/StringTokenizer.cs
  98. 1 1
      src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs
  99. 1 1
      src/Avalonia.Base/Utilities/ValueSpan.cs
  100. 0 4
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

+ 2 - 0
.editorconfig

@@ -144,6 +144,8 @@ dotnet_diagnostic.CS1591.severity = suggestion
 dotnet_diagnostic.CA1304.severity = warning
 # CA1802: Use literals where appropriate
 dotnet_diagnostic.CA1802.severity = warning
+# CA1815: Override equals and operator equals on value types
+dotnet_diagnostic.CA1815.severity = warning
 # CA1820: Test for empty strings using string length
 dotnet_diagnostic.CA1820.severity = warning
 # CA1821: Remove empty finalizers

+ 3 - 0
samples/ControlCatalog/MainView.xaml

@@ -135,6 +135,9 @@
       <TabItem Header="RadioButton">
         <pages:RadioButtonPage />
       </TabItem>
+      <TabItem Header="RefreshContainer">
+        <pages:RefreshContainerPage />
+      </TabItem>
       <TabItem Header="RelativePanel">
         <pages:RelativePanelPage />
       </TabItem>

+ 27 - 0
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@@ -0,0 +1,27 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:viewModels="using:ControlCatalog.ViewModels"
+             mc:Ignorable="d"
+             d:DesignWidth="800"
+             d:DesignHeight="450"
+             x:DataType="viewModels:RefreshContainerViewModel"
+             x:Class="ControlCatalog.Pages.RefreshContainerPage">
+  <DockPanel HorizontalAlignment="Stretch"
+             Height="600"
+             VerticalAlignment="Top">
+    <Label DockPanel.Dock="Top">A control that supports pull to refresh</Label>
+    <RefreshContainer Name="Refresh"
+                      DockPanel.Dock="Bottom"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch"
+                      PullDirection="TopToBottom"
+                      RefreshRequested="RefreshContainerPage_RefreshRequested"
+                      Margin="5">
+      <ListBox HorizontalAlignment="Stretch"
+               VerticalAlignment="Top"
+               Items="{Binding Items}"/>
+    </RefreshContainer>
+  </DockPanel>
+</UserControl>

+ 36 - 0
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@@ -0,0 +1,36 @@
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+    public class RefreshContainerPage : UserControl
+    {
+        private RefreshContainerViewModel _viewModel;
+
+        public RefreshContainerPage()
+        {
+            this.InitializeComponent();
+
+            _viewModel = new RefreshContainerViewModel();
+
+            DataContext = _viewModel;
+        }
+
+        private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
+        {
+            var deferral = e.GetDeferral();
+
+            await _viewModel.AddToTop();
+
+            deferral.Complete();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 26 - 0
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

@@ -0,0 +1,26 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia.Controls.Notifications;
+using ControlCatalog.Pages;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+    public class RefreshContainerViewModel : ViewModelBase
+    {
+        public ObservableCollection<string> Items { get; }
+
+        public RefreshContainerViewModel()
+        {
+            Items = new ObservableCollection<string>(Enumerable.Range(1, 200).Select(i => $"Item {i}"));
+        }
+
+        public async Task AddToTop()
+        {
+            await Task.Delay(3000);
+            Items.Insert(0, $"Item {200 - Items.Count}");
+        }
+    }
+}

+ 1 - 1
samples/RenderDemo/Pages/TextFormatterPage.axaml.cs

@@ -90,7 +90,7 @@ namespace RenderDemo.Pages
                     return new ControlRun(_control, _defaultProperties);
                 }
 
-                return new TextCharacters(_text.AsMemory(), _defaultProperties);
+                return new TextCharacters(_text, _defaultProperties);
             }
         }
 

+ 1 - 1
src/Android/Avalonia.Android/AndroidInputMethod.cs

@@ -167,7 +167,7 @@ namespace Avalonia.Android
         }
     }
 
-    public readonly struct ComposingRegion
+    public readonly record struct ComposingRegion
     {
         private readonly int _start = -1;
         private readonly int _end = -1;

+ 7 - 2
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -32,6 +32,7 @@ namespace Avalonia.Android
         public static AndroidPlatformOptions Options { get; private set; }
 
         internal static Compositor Compositor { get; private set; }
+        internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; }
 
         public static void Initialize()
         {
@@ -51,15 +52,19 @@ namespace Avalonia.Android
 
             if (Options.UseGpu)
             {
-                EglPlatformOpenGlInterface.TryInitialize();
+                EglPlatformGraphics.TryInitialize();
             }
             
             if (Options.UseCompositor)
             {
                 Compositor = new Compositor(
                     AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
-                    AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
+                    AvaloniaLocator.Current.GetService<IPlatformGraphics>());
             }
+            else
+                RenderInterface =
+                    new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current
+                        .GetService<IPlatformGraphics>());
         }
     }
 

+ 0 - 32
src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs

@@ -1,32 +0,0 @@
-using Avalonia.OpenGL;
-using Avalonia.OpenGL.Egl;
-using Avalonia.OpenGL.Surfaces;
-
-namespace Avalonia.Android.OpenGL
-{
-    internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase
-    {
-        private readonly EglPlatformOpenGlInterface _egl;
-        private readonly IEglWindowGlPlatformSurfaceInfo _info;
-
-        private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info)
-        {
-            _egl = egl;
-            _info = info;
-        }
-
-        public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
-            new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
-
-        public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
-        {
-            var feature = AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>();
-            if (feature is EglPlatformOpenGlInterface egl)
-            {
-                return new GlPlatformSurface(egl, info);
-            }
-
-            return null;
-        }
-    }
-}

+ 0 - 30
src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs

@@ -1,30 +0,0 @@
-using System;
-
-using Avalonia.OpenGL.Egl;
-using Avalonia.OpenGL.Surfaces;
-
-namespace Avalonia.Android.OpenGL
-{
-    internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
-    {
-        private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
-        private readonly EglSurface _surface;
-        private readonly IntPtr _handle;
-
-        public GlRenderTarget(
-            EglPlatformOpenGlInterface egl,
-            EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
-            EglSurface surface,
-            IntPtr handle)
-            : base(egl)
-        {
-            _info = info;
-            _surface = surface;
-            _handle = handle;
-        }
-
-        public bool IsCorrupted => _handle != _info.Handle;
-
-        public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
-    }
-}

+ 11 - 7
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -8,7 +8,6 @@ using Android.Runtime;
 using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
-using Avalonia.Android.OpenGL;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific.Helpers;
 using Avalonia.Android.Platform.Storage;
@@ -30,7 +29,7 @@ using AndroidRect = Android.Graphics.Rect;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
-    class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo,
+    class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
         ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
     {
         private readonly IGlPlatformSurface _gl;
@@ -47,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             _textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
             _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
             _pointerHelper = new AndroidMotionEventsHelper(this);
-            _gl = GlPlatformSurface.TryCreate(this);
+            _gl = new EglGlPlatformSurface(this);
             _framebuffer = new FramebufferManager(this);
 
             RenderScaling = _view.Scaling;
@@ -106,10 +105,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public IRenderer CreateRenderer(IRenderRoot root) =>
             AndroidPlatform.Options.UseCompositor
-                ? new CompositingRenderer(root, AndroidPlatform.Compositor)
+                ? new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces)
                 : AndroidPlatform.Options.UseDeferredRendering
-                    ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
-                    : new ImmediateRenderer((Visual)root);
+                    ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
+                            () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces),
+                            AndroidPlatform.RenderInterface)
+                        { RenderOnlyOnRenderThread = true }
+                    : new ImmediateRenderer((Visual)root,
+                        () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces),
+                        AndroidPlatform.RenderInterface);
 
         public virtual void Hide()
         {
@@ -283,7 +287,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
 
-        IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
+        IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
 
         public PixelSize Size => _view.Size;
 

+ 1 - 10
src/Avalonia.Base/Animation/Cue.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Animation
     /// Determines the time index for a <see cref="KeyFrame"/>. 
     /// </summary>
     [TypeConverter(typeof(CueTypeConverter))]
-    public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
+    public readonly record struct Cue : IEquatable<Cue>, IEquatable<double>
     {
         /// <summary>
         /// The normalized percent value, ranging from 0.0 to 1.0
@@ -49,15 +49,6 @@ namespace Avalonia.Animation
             }
         }
 
-        /// <summary>
-        /// Checks for equality between two <see cref="Cue"/>s.
-        /// </summary>
-        /// <param name="other">The second cue.</param>
-        public bool Equals(Cue other)
-        {
-            return CueValue == other.CueValue;
-        }
-
         /// <summary>
         /// Checks for equality between a <see cref="Cue"/>
         /// and a <see cref="double"/> value.

+ 1 - 1
src/Avalonia.Base/Data/BindingValue.cs

@@ -80,7 +80,7 @@ namespace Avalonia.Data
     /// - For an unset value, use <see cref="Unset"/> or simply `default`
     /// - For other types, call one of the static factory methods
     /// </remarks>
-    public readonly struct BindingValue<T>
+    public readonly record struct BindingValue<T>
     {
         private readonly T _value;
 

+ 152 - 0
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@@ -0,0 +1,152 @@
+using Avalonia.Input.GestureRecognizers;
+
+namespace Avalonia.Input
+{
+    public class PullGestureRecognizer : StyledElement, IGestureRecognizer
+    {
+        private IInputElement? _target;
+        private IGestureRecognizerActionsDispatcher? _actions;
+        private Point _initialPosition;
+        private int _gestureId;
+        private IPointer? _tracking;
+        private PullDirection _pullDirection;
+
+        /// <summary>
+        /// Defines the <see cref="PullDirection"/> property.
+        /// </summary>
+        public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty =
+            AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>(
+                nameof(PullDirection),
+                o => o.PullDirection,
+                (o, v) => o.PullDirection = v);
+
+        public PullDirection PullDirection
+        {
+            get => _pullDirection;
+            set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
+        }
+
+        public PullGestureRecognizer(PullDirection pullDirection)
+        {
+            PullDirection = pullDirection;
+        }
+
+        public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
+        {
+            _target = target;
+            _actions = actions;
+
+            _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
+            _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
+        }
+
+        private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+        {
+            PointerPressed(e);
+        }
+
+        private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+        {
+            PointerReleased(e);
+        }
+
+        public void PointerCaptureLost(IPointer pointer)
+        {
+            if (_tracking == pointer)
+            {
+                EndPull();
+            }
+        }
+
+        public void PointerMoved(PointerEventArgs e)
+        {
+            if (_tracking == e.Pointer && _target is Visual visual)
+            {
+                var currentPosition = e.GetPosition(visual);
+                _actions!.Capture(e.Pointer, this);
+
+                Vector delta = default;
+                switch (PullDirection)
+                {
+                    case PullDirection.TopToBottom:
+                        if (currentPosition.Y > _initialPosition.Y)
+                        {
+                            delta = new Vector(0, currentPosition.Y - _initialPosition.Y);
+                        }
+                        break;
+                    case PullDirection.BottomToTop:
+                        if (currentPosition.Y < _initialPosition.Y)
+                        {
+                            delta = new Vector(0, _initialPosition.Y - currentPosition.Y);
+                        }
+                        break;
+                    case PullDirection.LeftToRight:
+                        if (currentPosition.X > _initialPosition.X)
+                        {
+                            delta = new Vector(currentPosition.X - _initialPosition.X, 0);
+                        }
+                        break;
+                    case PullDirection.RightToLeft:
+                        if (currentPosition.X < _initialPosition.X)
+                        {
+                            delta = new Vector(_initialPosition.X - currentPosition.X, 0);
+                        }
+                        break;
+                }
+
+                _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
+            }
+        }
+
+        public void PointerPressed(PointerPressedEventArgs e)
+        {
+            if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
+            {
+                var position = e.GetPosition(visual);
+
+                var canPull = false;
+
+                var bounds = visual.Bounds;
+
+                switch (PullDirection)
+                {
+                    case PullDirection.TopToBottom:
+                        canPull = position.Y < bounds.Height * 0.1;
+                        break;
+                    case PullDirection.BottomToTop:
+                        canPull = position.Y > bounds.Height - (bounds.Height * 0.1);
+                        break;
+                    case PullDirection.LeftToRight:
+                        canPull = position.X < bounds.Width * 0.1;
+                        break;
+                    case PullDirection.RightToLeft:
+                        canPull = position.X > bounds.Width - (bounds.Width * 0.1);
+                        break;
+                }
+
+                if (canPull)
+                {
+                    _gestureId = PullGestureEventArgs.GetNextFreeId();
+                    _tracking = e.Pointer;
+                    _initialPosition = position;
+                }
+            }
+        }
+
+        public void PointerReleased(PointerReleasedEventArgs e)
+        {
+            if (_tracking == e.Pointer)
+            {
+                EndPull();
+            }
+        }
+
+        private void EndPull()
+        {
+            _tracking = null;
+            _initialPosition = default;
+
+            _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
+        }
+    }
+}

+ 8 - 0
src/Avalonia.Base/Input/Gestures.cs

@@ -46,6 +46,14 @@ namespace Avalonia.Input
         private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
         private static Point s_lastPressPoint;
 
+        public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent =
+            RoutedEvent.Register<PullGestureEventArgs>(
+                "PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
+
+        public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
+            RoutedEvent.Register<PullGestureEndedEventArgs>(
+                "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
+
         static Gestures()
         {
             InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);

+ 5 - 0
src/Avalonia.Base/Input/InputElement.cs

@@ -442,6 +442,11 @@ namespace Avalonia.Input
             {
                 SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
                 PseudoClasses.Set(":disabled", !value);
+
+                if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this)
+                {
+                    FocusManager.Instance?.Focus(null);
+                }
             }
         }
 

+ 2 - 2
src/Avalonia.Base/Input/PointerPoint.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Input
     /// <summary>
     /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact.
     /// </summary>
-    public struct PointerPoint
+    public record struct PointerPoint
     {
         public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
         {
@@ -33,7 +33,7 @@ namespace Avalonia.Input
     /// <summary>
     /// Provides extended properties for a PointerPoint object.
     /// </summary>
-    public struct PointerPointProperties
+    public record struct PointerPointProperties
     {
         /// <summary>
         /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.

+ 43 - 0
src/Avalonia.Base/Input/PullGestureEventArgs.cs

@@ -0,0 +1,43 @@
+using System;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+    public class PullGestureEventArgs : RoutedEventArgs
+    {
+        public int Id { get; }
+        public Vector Delta { get; }
+        public PullDirection PullDirection { get; }
+
+        private static int _nextId = 1;
+
+        internal static int GetNextFreeId() => _nextId++;
+        
+        public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent)
+        {
+            Id = id;
+            Delta = delta;
+            PullDirection = pullDirection;
+        }
+    }
+
+    public class PullGestureEndedEventArgs : RoutedEventArgs
+    {
+        public int Id { get; }
+        public PullDirection PullDirection { get; }
+
+        public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent)
+        {
+            Id = id;
+            PullDirection = pullDirection;
+        }
+    }
+
+    public enum PullDirection
+    {
+        TopToBottom,
+        BottomToTop,
+        LeftToRight,
+        RightToLeft
+    }
+}

+ 1 - 1
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@@ -130,7 +130,7 @@ namespace Avalonia.Input.Raw
         internal IInputElement? InputHitTestResult { get; set; }
     }
 
-    public struct RawPointerPoint
+    public record struct RawPointerPoint
     {
         /// <summary>
         /// Pointer position, in client DIPs.

+ 1 - 1
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@@ -46,7 +46,7 @@ namespace Avalonia.Input.TextInput
         void SelectInSurroundingText(int start, int end);
     }
 
-    public struct TextInputMethodSurroundingText
+    public record struct TextInputMethodSurroundingText
     {
         public string Text { get; set; }
         public int CursorOffset { get; set; }

+ 1 - 1
src/Avalonia.Base/Logging/ParametrizedLogger.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Logging
     /// <summary>
     /// Logger sink parametrized for given logging level.
     /// </summary>
-    public readonly struct ParametrizedLogger
+    public readonly record struct ParametrizedLogger
     {
         private readonly ILogSink _sink;
         private readonly LogEventLevel _level;

+ 1 - 1
src/Avalonia.Base/Matrix.cs

@@ -571,7 +571,7 @@ namespace Avalonia
             return true;
         }
 
-        public struct Decomposed
+        public record struct Decomposed
         {
             public Vector Translate;
             public Vector Scale;

+ 6 - 0
src/Avalonia.Base/Media/BoxShadow.cs

@@ -171,5 +171,11 @@ namespace Avalonia.Media
 
         public Rect TransformBounds(in Rect rect)
             => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
+
+        public static bool operator ==(BoxShadow left, BoxShadow right) =>
+            left.Equals(right);
+
+        public static bool operator !=(BoxShadow left, BoxShadow right) => 
+            !(left == right);
     }
 }

+ 8 - 0
src/Avalonia.Base/Media/BoxShadows.cs

@@ -62,7 +62,9 @@ namespace Avalonia.Media
         }
 
         [EditorBrowsable(EditorBrowsableState.Never)]
+#pragma warning disable CA1815 // Override equals and operator equals on value types
         public struct BoxShadowsEnumerator
+#pragma warning restore CA1815 // Override equals and operator equals on value types
         {
             private int _index;
             private BoxShadows _shadows;
@@ -149,5 +151,11 @@ namespace Avalonia.Media
                 return hashCode;
             }
         }
+
+        public static bool operator ==(BoxShadows left, BoxShadows right) => 
+            left.Equals(right);
+
+        public static bool operator !=(BoxShadows left, BoxShadows right) =>
+            !(left == right);
     }
 }

+ 1 - 1
src/Avalonia.Base/Media/DrawingContext.cs

@@ -261,7 +261,7 @@ namespace Avalonia.Media
             DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
         }
 
-        public readonly struct PushedState : IDisposable
+        public readonly record struct PushedState : IDisposable
         {
             private readonly int _level;
             private readonly DrawingContext _context;

+ 1 - 1
src/Avalonia.Base/Media/FontMetrics.cs

@@ -3,7 +3,7 @@
     /// <summary>
     /// The font metrics is holding information about a font's ascent, descent, etc. in design em units.
     /// </summary>
-    public readonly struct FontMetrics
+    public readonly record struct FontMetrics
     {
         /// <summary>
         ///     Gets the font design units per em.

+ 2 - 6
src/Avalonia.Base/Media/FormattedText.cs

@@ -1,10 +1,8 @@
 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;
 
@@ -25,7 +23,7 @@ namespace Avalonia.Media
         private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
 
         // properties and format runs
-        private ReadOnlySlice<char> _text;
+        private string _text;
         private readonly SpanVector _formatRuns = new SpanVector(null);
         private SpanPosition _latestPosition;
 
@@ -69,9 +67,7 @@ namespace Avalonia.Media
 
             ValidateFontSize(emSize);
 
-            _text = textToFormat != null ?
-                new ReadOnlySlice<char>(textToFormat.AsMemory()) :
-                throw new ArgumentNullException(nameof(textToFormat));
+            _text = textToFormat;
 
             var runProps = new GenericTextRunProperties(
                 typeface,

+ 1 - 1
src/Avalonia.Base/Media/GlyphMetrics.cs

@@ -1,6 +1,6 @@
 namespace Avalonia.Media;
 
-public readonly struct GlyphMetrics
+public readonly record struct GlyphMetrics
 {
     /// <summary>
     /// Distance from the x-origin to the left extremum of the glyph.

+ 236 - 183
src/Avalonia.Base/Media/GlyphRun.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Drawing;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
 using Avalonia.Utilities;
@@ -22,15 +21,12 @@ namespace Avalonia.Media
         private Point? _baselineOrigin;
         private GlyphRunMetrics? _glyphRunMetrics;
 
-        private ReadOnlySlice<char> _characters;
-
+        private IReadOnlyList<char> _characters;
         private IReadOnlyList<ushort> _glyphIndices;
         private IReadOnlyList<double>? _glyphAdvances;
         private IReadOnlyList<Vector>? _glyphOffsets;
         private IReadOnlyList<int>? _glyphClusters;
 
-        private int _offsetToFirstCharacter;
-
         /// <summary>
         ///     Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
         /// </summary>
@@ -45,7 +41,7 @@ namespace Avalonia.Media
         public GlyphRun(
             IGlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
-            ReadOnlySlice<char> characters,
+            IReadOnlyList<char> characters,
             IReadOnlyList<ushort> glyphIndices,
             IReadOnlyList<double>? glyphAdvances = null,
             IReadOnlyList<Vector>? glyphOffsets = null,
@@ -54,19 +50,19 @@ namespace Avalonia.Media
         {
             _glyphTypeface = glyphTypeface;
 
-            FontRenderingEmSize = fontRenderingEmSize;
+            _fontRenderingEmSize = fontRenderingEmSize;
 
-            Characters = characters;
+            _characters = characters;
 
             _glyphIndices = glyphIndices;
 
-            GlyphAdvances = glyphAdvances;
+            _glyphAdvances = glyphAdvances;
 
-            GlyphOffsets = glyphOffsets;
+            _glyphOffsets = glyphOffsets;
 
-            GlyphClusters = glyphClusters;
+            _glyphClusters = glyphClusters;
 
-            BiDiLevel = biDiLevel;
+            _biDiLevel = biDiLevel;
         }
 
         /// <summary>
@@ -145,7 +141,7 @@ namespace Avalonia.Media
         /// <summary>
         ///     Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
         /// </summary>
-        public ReadOnlySlice<char> Characters
+        public IReadOnlyList<char> Characters
         {
             get => _characters;
             set => Set(ref _characters, value);
@@ -219,7 +215,7 @@ namespace Avalonia.Media
         /// </returns>
         public double GetDistanceFromCharacterHit(CharacterHit characterHit)
         {
-            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
+            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
 
             var distance = 0.0;
 
@@ -227,12 +223,12 @@ namespace Avalonia.Media
             {
                 if (GlyphClusters != null)
                 {
-                    if (characterIndex < GlyphClusters[0])
+                    if (characterIndex < Metrics.FirstCluster)
                     {
                         return 0;
                     }
 
-                    if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
+                    if (characterIndex > Metrics.LastCluster)
                     {
                         return Metrics.WidthIncludingTrailingWhitespace;
                     }
@@ -268,12 +264,12 @@ namespace Avalonia.Media
 
                 if (GlyphClusters != null && GlyphClusters.Count > 0)
                 {
-                    if (characterIndex > GlyphClusters[0])
+                    if (characterIndex > Metrics.LastCluster)
                     {
                         return 0;
                     }
 
-                    if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
+                    if (characterIndex <= Metrics.FirstCluster)
                     {
                         return Size.Width;
                     }
@@ -299,19 +295,12 @@ namespace Avalonia.Media
         /// </returns>
         public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
         {
-            var characterIndex = 0;
-
             // Before
             if (distance <= 0)
             {
                 isInside = false;
 
-                if (GlyphClusters != null)
-                {
-                    characterIndex = GlyphClusters[characterIndex];
-                }
-
-                var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
+                var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _);
 
                 return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
             }
@@ -321,18 +310,13 @@ namespace Avalonia.Media
             {
                 isInside = false;
 
-                characterIndex = GlyphIndices.Count - 1;
-
-                if (GlyphClusters != null)
-                {
-                    characterIndex = GlyphClusters[characterIndex];
-                }
-
-                var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
+                var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _);
 
                 return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
             }
 
+            var characterIndex = 0;
+
             //Within
             var currentX = 0d;
 
@@ -378,7 +362,7 @@ namespace Avalonia.Media
             var characterHit = FindNearestCharacterHit(characterIndex, out var width);
 
             var delta = width / 2;
-            
+
             var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3);
 
             var isTrailing = offset > delta;
@@ -400,24 +384,15 @@ namespace Avalonia.Media
             {
                 characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
 
-                var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-
-                return textPosition > _characters.End ?
-                    characterHit :
-                    new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
-            }
-
-            var nextCharacterHit =
-                FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+                if (characterHit.FirstCharacterIndex == Metrics.LastCluster)
+                {
+                    return characterHit;
+                }
 
-            if (characterHit == nextCharacterHit)
-            {
-                return characterHit;
+                return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
             }
 
-            return characterHit.TrailingLength > 0 ?
-                nextCharacterHit :
-                new CharacterHit(nextCharacterHit.FirstCharacterIndex);
+            return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
         }
 
         /// <summary>
@@ -454,29 +429,24 @@ namespace Avalonia.Media
                 return characterIndex;
             }
 
-            if (IsLeftToRight)
+            if (characterIndex > Metrics.LastCluster)
             {
-                if (characterIndex < GlyphClusters[0])
+                if (IsLeftToRight)
                 {
-                    return 0;
+                    return GlyphIndices.Count - 1;
                 }
 
-                if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
-                {
-                    return GlyphClusters.Count - 1;
-                }
+                return 0;
             }
-            else
-            {
-                if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
-                {
-                    return GlyphClusters.Count - 1;
-                }
 
-                if (characterIndex > GlyphClusters[0])
+            if (characterIndex < Metrics.FirstCluster)
+            {
+                if (IsLeftToRight)
                 {
                     return 0;
                 }
+
+                return GlyphIndices.Count - 1;
             }
 
             var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
@@ -498,7 +468,7 @@ namespace Avalonia.Media
 
                 if (start < 0)
                 {
-                    return -1;
+                    goto result;
                 }
             }
 
@@ -517,6 +487,18 @@ namespace Avalonia.Media
                 }
             }
 
+        result:
+
+            if (start < 0)
+            {
+                return 0;
+            }
+
+            if (start > GlyphIndices.Count - 1)
+            {
+                return GlyphIndices.Count - 1;
+            }
+
             return start;
         }
 
@@ -532,20 +514,20 @@ namespace Avalonia.Media
         {
             width = 0.0;
 
-            var start = FindGlyphIndex(index);
+            var glyphIndex = FindGlyphIndex(index);
 
             if (GlyphClusters == null)
             {
                 width = GetGlyphAdvance(index, out _);
 
-                return new CharacterHit(start, 1);
+                return new CharacterHit(glyphIndex, 1);
             }
 
-            var cluster = GlyphClusters[start];
+            var cluster = GlyphClusters[glyphIndex];
 
             var nextCluster = cluster;
 
-            var currentIndex = start;
+            var currentIndex = glyphIndex;
 
             while (nextCluster == cluster)
             {
@@ -571,20 +553,64 @@ namespace Avalonia.Media
                 }
 
                 nextCluster = GlyphClusters[currentIndex];
-            }           
+            }
 
-            int trailingLength;
+            var clusterLength = Math.Max(0, nextCluster - cluster);
 
-            if (nextCluster == cluster)
-            {
-                trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
-            }
-            else
+            if (cluster == Metrics.LastCluster && clusterLength == 0)
             {
-                trailingLength = nextCluster - cluster;
+                var characterLength = 0;
+
+                var currentCluster = Metrics.FirstCluster;
+
+                if (IsLeftToRight)
+                {
+                    for (int i = 1; i < GlyphClusters.Count; i++)
+                    {
+                        nextCluster = GlyphClusters[i];
+
+                        if (currentCluster > cluster)
+                        {
+                            break;
+                        }
+
+                        var length = nextCluster - currentCluster;
+
+                        characterLength += length;
+
+                        currentCluster = nextCluster;
+                    }
+                }
+                else
+                {
+                    for (int i = GlyphClusters.Count - 1; i >= 0; i--)
+                    {
+                        nextCluster = GlyphClusters[i];
+
+                        if (currentCluster > cluster)
+                        {
+                            break;
+                        }
+
+                        var length = nextCluster - currentCluster;
+
+                        characterLength += length;
+
+                        currentCluster = nextCluster;
+                    }
+                }
+
+                if (Characters != null)
+                {
+                    clusterLength = Characters.Count - characterLength;
+                }
+                else
+                {
+                    clusterLength = 1;
+                }
             }
 
-            return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
+            return new CharacterHit(cluster, clusterLength);
         }
 
         /// <summary>
@@ -618,22 +644,25 @@ namespace Avalonia.Media
 
         private GlyphRunMetrics CreateGlyphRunMetrics()
         {
-            var firstCluster = 0;
-            var lastCluster = Characters.Length - 1;
+            int firstCluster = 0, lastCluster = 0;
 
-            if (!IsLeftToRight)
+            if (_glyphClusters != null && _glyphClusters.Count > 0)
             {
-                var cluster = firstCluster;
-                firstCluster = lastCluster;
-                lastCluster = cluster;
+                firstCluster = _glyphClusters[0];
+                lastCluster = _glyphClusters[_glyphClusters.Count - 1];
             }
-
-            if (GlyphClusters != null && GlyphClusters.Count > 0)
+            else
             {
-                firstCluster = GlyphClusters[0];
-                lastCluster = GlyphClusters[GlyphClusters.Count - 1];
+                if (Characters != null && Characters.Count > 0)
+                {
+                    firstCluster = 0;
+                    lastCluster = Characters.Count - 1;
+                }
+            }
 
-                _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
+            if (!IsLeftToRight)
+            {
+                (lastCluster, firstCluster) = (firstCluster, lastCluster);
             }
 
             var isReversed = firstCluster > lastCluster;
@@ -666,12 +695,19 @@ namespace Avalonia.Media
                 }
             }
 
-            return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
-                height);
+            return new GlyphRunMetrics(
+                width,
+                widthIncludingTrailingWhitespace,
+                height,
+                trailingWhitespaceLength,
+                newLineLength,
+                firstCluster,
+                lastCluster
+            );
         }
 
         private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
-        {          
+        {
             if (isReversed)
             {
                 return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
@@ -681,66 +717,82 @@ namespace Avalonia.Media
             newLineLength = 0;
             var trailingWhitespaceLength = 0;
 
-            if (GlyphClusters == null)
+            if (Characters != null)
             {
-                for (var i = _characters.Length - 1; i >= 0;)
+                if (GlyphClusters == null)
                 {
-                    var codepoint = Codepoint.ReadAt(_characters, i, out var count);
-
-                    if (!codepoint.IsWhiteSpace)
+                    for (var i = _characters.Count - 1; i >= 0;)
                     {
-                        break;
-                    }
+                        var codepoint = Codepoint.ReadAt(_characters, i, out var count);
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength++;
-                    }
+                        if (!codepoint.IsWhiteSpace)
+                        {
+                            break;
+                        }
 
-                    trailingWhitespaceLength++;
+                        if (codepoint.IsBreakChar)
+                        {
+                            newLineLength++;
+                        }
+
+                        trailingWhitespaceLength++;
 
-                    i -= count;
-                    glyphCount++;
+                        i -= count;
+                        glyphCount++;
+                    }
                 }
-            }
-            else
-            {
-                for (var i = GlyphClusters.Count - 1; i >= 0; i--)
+                else
                 {
-                    var currentCluster = GlyphClusters[i];
-                    var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
-                    var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
-
-                    if (!codepoint.IsWhiteSpace)
+                    if (Characters.Count > 0)
                     {
-                        break;
-                    }
+                        var characterIndex = Characters.Count - 1;
 
-                    var clusterLength = 1;
+                        for (var i = GlyphClusters.Count - 1; i >= 0; i--)
+                        {
+                            var currentCluster = GlyphClusters[i];
+                            var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
 
-                    while(i - 1 >= 0)
-                    {
-                        var nextCluster = GlyphClusters[i - 1];
+                            characterIndex -= characterLength;
 
-                        if(currentCluster == nextCluster)
-                        {
-                            clusterLength++;
-                            i--;
+                            if (!codepoint.IsWhiteSpace)
+                            {
+                                break;
+                            }
 
-                            continue;
-                        }
+                            var clusterLength = 1;
 
-                        break;
-                    }
+                            while (i - 1 >= 0)
+                            {
+                                var nextCluster = GlyphClusters[i - 1];
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength += clusterLength;
-                    }
+                                if (currentCluster == nextCluster)
+                                {
+                                    clusterLength++;
+                                    i--;
+
+                                    if(characterIndex >= 0)
+                                    {
+                                        codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
+
+                                        characterIndex -= characterLength;
+                                    }
+
+                                    continue;
+                                }
+
+                                break;
+                            }
+
+                            if (codepoint.IsBreakChar)
+                            {
+                                newLineLength += clusterLength;
+                            }
 
-                    trailingWhitespaceLength += clusterLength;
-                   
-                    glyphCount++;                   
+                            trailingWhitespaceLength += clusterLength;
+
+                            glyphCount++;
+                        }
+                    }
                 }
             }
 
@@ -753,67 +805,73 @@ namespace Avalonia.Media
             newLineLength = 0;
             var trailingWhitespaceLength = 0;
 
-            if (GlyphClusters == null)
+            if (Characters != null)
             {
-                for (var i = 0; i < Characters.Length;)
+                if (GlyphClusters == null)
                 {
-                    var codepoint = Codepoint.ReadAt(_characters, i, out var count);
-
-                    if (!codepoint.IsWhiteSpace)
+                    for (var i = 0; i < Characters.Count;)
                     {
-                        break;
-                    }
+                        var codepoint = Codepoint.ReadAt(_characters, i, out var count);
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength++;
-                    }
+                        if (!codepoint.IsWhiteSpace)
+                        {
+                            break;
+                        }
 
-                    trailingWhitespaceLength++;
+                        if (codepoint.IsBreakChar)
+                        {
+                            newLineLength++;
+                        }
 
-                    i += count;
-                    glyphCount++;
+                        trailingWhitespaceLength++;
+
+                        i += count;
+                        glyphCount++;
+                    }
                 }
-            }
-            else
-            {
-                for (var i = 0; i < GlyphClusters.Count; i++)
+                else
                 {
-                    var currentCluster = GlyphClusters[i];
-                    var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
-                    var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
+                    var characterIndex = 0;
 
-                    if (!codepoint.IsWhiteSpace)
+                    for (var i = 0; i < GlyphClusters.Count; i++)
                     {
-                        break;
-                    }
+                        var currentCluster = GlyphClusters[i];
+                        var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
 
-                    var clusterLength = 1;
+                        characterIndex += characterLength;
 
-                    var j = i;
+                        if (!codepoint.IsWhiteSpace)
+                        {
+                            break;
+                        }
 
-                    while (j - 1 >= 0)
-                    {
-                        var nextCluster = GlyphClusters[--j];
+                        var clusterLength = 1;
 
-                        if (currentCluster == nextCluster)
+                        var j = i;
+
+                        while (j - 1 >= 0)
                         {
-                            clusterLength++;                        
+                            var nextCluster = GlyphClusters[--j];
 
-                            continue;
-                        }
+                            if (currentCluster == nextCluster)
+                            {
+                                clusterLength++;
 
-                        break;
-                    }
+                                continue;
+                            }
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength += clusterLength;
-                    }
+                            break;
+                        }
+
+                        if (codepoint.IsBreakChar)
+                        {
+                            newLineLength += clusterLength;
+                        }
 
-                    trailingWhitespaceLength += clusterLength;
+                        trailingWhitespaceLength += clusterLength;
 
-                    glyphCount += clusterLength;
+                        glyphCount += clusterLength;
+                    }
                 }
             }
 
@@ -855,14 +913,9 @@ namespace Avalonia.Media
                 throw new InvalidOperationException();
             }
 
-            _glyphRunImpl = CreateGlyphRunImpl();
-        }
-
-        private IGlyphRunImpl CreateGlyphRunImpl()
-        {
             var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
 
-            return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
+            _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
         }
 
         void IDisposable.Dispose()

+ 13 - 7
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@@ -1,25 +1,31 @@
 namespace Avalonia.Media
 {
-    public readonly struct GlyphRunMetrics
+    public readonly record struct GlyphRunMetrics
     {
-        public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
-            int newlineLength, double height)
+        public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
+            int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
         {
             Width = width;
             WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
-            TrailingWhitespaceLength = trailingWhitespaceLength;
-            NewlineLength = newlineLength;
             Height = height;
+            TrailingWhitespaceLength = trailingWhitespaceLength;
+            NewLineLength= newLineLength;
+            FirstCluster = firstCluster;
+            LastCluster = lastCluster;
         }
 
         public double Width { get; }
 
         public double WidthIncludingTrailingWhitespace { get; }
 
+        public double Height { get; }
+
         public int TrailingWhitespaceLength { get; }
         
-        public int NewlineLength { get; }
+        public int NewLineLength { get;  }
 
-        public double Height { get; }
+        public int FirstCluster { get; }
+
+        public int LastCluster { get; }
     }
 }

+ 2 - 0
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@@ -60,5 +60,7 @@ namespace Avalonia.Media.Imaging
 
         /// <inheritdoc/>
         public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr);
+
+        bool IRenderTarget.IsCorrupted => false;
     }
 }

+ 1 - 1
src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs

@@ -2,7 +2,7 @@
 
 namespace Avalonia.Media
 {
-    public readonly struct TextCollapsingCreateInfo
+    public readonly record struct TextCollapsingCreateInfo
     {
         public readonly double Width;
         public readonly TextRunProperties TextRunProperties;

+ 293 - 0
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@@ -0,0 +1,293 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+    public readonly struct CharacterBufferRange : IReadOnlyList<char>
+    {
+        /// <summary>
+        /// Getting an empty character string
+        /// </summary>
+        public static CharacterBufferRange Empty => new CharacterBufferRange();
+
+        /// <summary>
+        /// Construct <see cref="CharacterBufferRange"/> from character array
+        /// </summary>
+        /// <param name="characterArray">character array</param>
+        /// <param name="offsetToFirstChar">character buffer offset to the first character</param>
+        /// <param name="characterLength">character length</param>
+        public CharacterBufferRange(
+            char[] characterArray,
+            int offsetToFirstChar,
+            int characterLength
+            )
+            : this(
+                new CharacterBufferReference(characterArray, offsetToFirstChar),
+                characterLength
+                )
+        { }
+
+        /// <summary>
+        /// Construct <see cref="CharacterBufferRange"/> from string
+        /// </summary>
+        /// <param name="characterString">character string</param>
+        /// <param name="offsetToFirstChar">character buffer offset to the first character</param>
+        /// <param name="characterLength">character length</param>
+        public CharacterBufferRange(
+            string characterString,
+            int offsetToFirstChar,
+            int characterLength
+            )
+            : this(
+                new CharacterBufferReference(characterString, offsetToFirstChar),
+                characterLength
+                )
+        { }
+
+        /// <summary>
+        /// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
+        /// </summary>
+        /// <param name="characterBufferReference">character buffer reference</param>
+        /// <param name="characterLength">number of characters</param>
+        public CharacterBufferRange(
+            CharacterBufferReference characterBufferReference,
+            int characterLength
+            )
+        {
+            if (characterLength < 0)
+            {
+                throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
+            }
+
+            int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
+                characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
+                0;
+
+            if (characterLength > maxLength)
+            {
+                throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
+            }
+
+            CharacterBufferReference = characterBufferReference;
+            Length = characterLength;
+        }
+
+        /// <summary>
+        /// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
+        /// </summary>
+        internal CharacterBufferRange(
+            CharacterBufferRange characterBufferRange,
+            int offsetToFirstChar,
+            int characterLength
+            ) :
+            this(
+                characterBufferRange.CharacterBuffer,
+                characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
+                characterLength
+                )
+        { }
+
+
+        /// <summary>
+        /// Construct a <see cref="CharacterBufferRange"/> from string
+        /// </summary>
+        internal CharacterBufferRange(
+            string charString
+            ) :
+            this(
+                charString,
+                0,
+                charString.Length
+                )
+        { }
+
+
+        /// <summary>
+        /// Construct <see cref="CharacterBufferRange"/> from memory buffer
+        /// </summary>
+        internal CharacterBufferRange(
+            ReadOnlyMemory<char> charBuffer,
+            int offsetToFirstChar,
+            int characterLength
+            ) :
+            this(
+                new CharacterBufferReference(charBuffer, offsetToFirstChar),
+                characterLength
+                )
+        { }
+
+
+        /// <summary>
+        /// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
+        /// </summary>
+        internal CharacterBufferRange(TextRun textRun)
+        {
+            CharacterBufferReference = textRun.CharacterBufferReference;
+            Length = textRun.Length;
+        }
+
+        public char this[int index]
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get
+            {
+#if DEBUG
+                if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(index));
+                }
+#endif
+                return Span[index];
+            }
+        }
+
+        /// <summary>
+        /// Gets a reference to the character buffer
+        /// </summary>
+        public CharacterBufferReference CharacterBufferReference { get; }
+
+        /// <summary>
+        /// Gets the number of characters in text source character store
+        /// </summary>
+        public int Length { get; }
+
+        /// <summary>
+        /// Gets a span from the character buffer range
+        /// </summary>
+        public ReadOnlySpan<char> Span =>
+            CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
+
+        /// <summary>
+        /// Gets the character memory buffer
+        /// </summary>
+        internal ReadOnlyMemory<char> CharacterBuffer
+        {
+            get { return CharacterBufferReference.CharacterBuffer; }
+        }
+
+        /// <summary>
+        /// Gets the character offset relative to the beginning of buffer to 
+        /// the first character of the run
+        /// </summary>
+        internal int OffsetToFirstChar
+        {
+            get { return CharacterBufferReference.OffsetToFirstChar; }
+        }
+
+        /// <summary>
+        /// Indicate whether the character buffer range is empty
+        /// </summary>
+        internal bool IsEmpty
+        {
+            get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; }
+        }
+
+        internal CharacterBufferRange Take(int length)
+        {
+            if (IsEmpty)
+            {
+                return this;
+            }
+
+            if (length > Length)
+            {
+                throw new ArgumentOutOfRangeException(nameof(length));
+            }
+
+            return new CharacterBufferRange(CharacterBufferReference, length);
+        }
+
+        internal CharacterBufferRange Skip(int length)
+        {
+            if (IsEmpty)
+            {
+                return this;
+            }
+
+            if (length > Length)
+            {
+                throw new ArgumentOutOfRangeException(nameof(length));
+            }
+
+            if (length == Length)
+            {
+                return new CharacterBufferRange(new CharacterBufferReference(), 0);
+            }
+
+            var characterBufferReference = new CharacterBufferReference(
+                CharacterBufferReference.CharacterBuffer,
+                CharacterBufferReference.OffsetToFirstChar + length);
+
+            return new CharacterBufferRange(characterBufferReference, Length - length);
+        }
+
+        /// <summary>
+        /// Compute hash code
+        /// </summary>
+        public override int GetHashCode()
+        {
+            return CharacterBufferReference.GetHashCode() ^ Length;
+        }
+
+        /// <summary>
+        /// Test equality with the input object
+        /// </summary>
+        /// <param name="obj"> The object to test </param>
+        public override bool Equals(object? obj)
+        {
+            if (obj is CharacterBufferRange range)
+            {
+                return Equals(range);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Test equality with the input CharacterBufferRange
+        /// </summary>
+        /// <param name="value"> The CharacterBufferRange value to test </param>
+        public bool Equals(CharacterBufferRange value)
+        {
+            return CharacterBufferReference.Equals(value.CharacterBufferReference)
+                && Length == value.Length;
+        }
+
+        /// <summary>
+        /// Compare two CharacterBufferRange for equality
+        /// </summary>
+        /// <param name="left">left operand</param>
+        /// <param name="right">right operand</param>
+        /// <returns>whether or not two operands are equal</returns>
+        public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
+        {
+            return left.Equals(right);
+        }
+
+        /// <summary>
+        /// Compare two CharacterBufferRange for inequality
+        /// </summary>
+        /// <param name="left">left operand</param>
+        /// <param name="right">right operand</param>
+        /// <returns>whether or not two operands are equal</returns>
+        public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
+        {
+            return !(left == right);
+        }
+
+        int IReadOnlyCollection<char>.Count => Length;
+
+        public IEnumerator<char> GetEnumerator()
+        {
+            return new ImmutableReadOnlyListStructEnumerator<char>(this);
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+    }
+}

+ 115 - 0
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@@ -0,0 +1,115 @@
+using System;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Text character buffer reference
+    /// </summary>
+    public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
+    {
+        /// <summary>
+        /// Construct character buffer reference from character array
+        /// </summary>
+        /// <param name="characterArray">character array</param>
+        /// <param name="offsetToFirstChar">character buffer offset to the first character</param>
+        public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
+            : this(characterArray.AsMemory(), offsetToFirstChar)
+        { }
+
+        /// <summary>
+        /// Construct character buffer reference from string
+        /// </summary>
+        /// <param name="characterString">character string</param>
+        /// <param name="offsetToFirstChar">character buffer offset to the first character</param>
+        public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
+            : this(characterString.AsMemory(), offsetToFirstChar)
+        { }
+      
+        /// <summary>
+        /// Construct character buffer reference from memory buffer
+        /// </summary>
+        internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
+        {
+            if (offsetToFirstChar < 0)
+            {
+                throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
+            }
+
+            // maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
+            // even in the case of an empty or null character buffer
+            var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
+            if (offsetToFirstChar > maxOffset)
+            {
+                throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
+            }
+
+            CharacterBuffer = characterBuffer;
+            OffsetToFirstChar = offsetToFirstChar;
+        }
+
+        /// <summary>
+        /// Gets the character memory buffer
+        /// </summary>
+        public ReadOnlyMemory<char> CharacterBuffer { get; }
+
+        /// <summary>
+        /// Gets the character offset relative to the beginning of buffer to 
+        /// the first character of the run
+        /// </summary>
+        public int OffsetToFirstChar { get; }
+
+        /// <summary>
+        /// Compute hash code
+        /// </summary>
+        public override int GetHashCode()
+        {
+            return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
+        }
+
+        /// <summary>
+        /// Test equality with the input object 
+        /// </summary>
+        /// <param name="obj"> The object to test. </param>
+        public override bool Equals(object? obj)
+        {
+            if (obj is CharacterBufferReference reference)
+            {
+                return Equals(reference);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Test equality with the input CharacterBufferReference
+        /// </summary>
+        /// <param name="value"> The characterBufferReference value to test </param>
+        public bool Equals(CharacterBufferReference value)
+        {
+            return CharacterBuffer.Equals(value.CharacterBuffer);
+        }
+
+        /// <summary>
+        /// Compare two CharacterBufferReference for equality
+        /// </summary>
+        /// <param name="left">left operand</param>
+        /// <param name="right">right operand</param>
+        /// <returns>whether or not two operands are equal</returns>
+        public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
+        {
+            return left.Equals(right);
+        }
+
+        /// <summary>
+        /// Compare two CharacterBufferReference for inequality
+        /// </summary>
+        /// <param name="left">left operand</param>
+        /// <param name="right">right operand</param>
+        /// <returns>whether or not two operands are equal</returns>
+        public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
+        {
+            return !(left == right);
+        }
+    }
+}
+

+ 7 - 6
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting
 {
     internal readonly struct FormattedTextSource : ITextSource
     {
-        private readonly ReadOnlySlice<char> _text;
+        private readonly CharacterBufferRange _text;
+        private readonly int length;
         private readonly TextRunProperties _defaultProperties;
         private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
 
-        public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
+        public FormattedTextSource(string text, TextRunProperties defaultProperties,
             IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
         {
-            _text = text;
+            _text = new CharacterBufferRange(text);
             _defaultProperties = defaultProperties;
             _textModifier = textModifier;
         }
@@ -35,7 +36,7 @@ namespace Avalonia.Media.TextFormatting
 
             var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
 
-            return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
+            return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value);
         }
 
         /// <summary>
@@ -48,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>
         /// The created text style run.
         /// </returns>
-        private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text, int firstTextSourceIndex,
+        private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex,
             TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
         {
             if (textModifier == null || textModifier.Count == 0)
@@ -122,7 +123,7 @@ namespace Avalonia.Media.TextFormatting
             return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
         }
 
-        private static int CoerceLength(ReadOnlySlice<char> text, int length)
+        private static int CoerceLength(CharacterBufferRange text, int length)
         {
             var finalLength = 0;
 

+ 12 - 7
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting
 
             var breakOportunities = new Queue<int>();
 
+            var currentPosition = textLine.FirstTextSourceIndex;
+
             foreach (var textRun in lineImpl.TextRuns)
             {
-                var text = textRun.Text;
+                var text = new CharacterBufferRange(textRun);
 
                 if (text.IsEmpty)
                 {
                     continue;
                 }
 
-                var start = text.Start;
-
                 var lineBreakEnumerator = new LineBreakEnumerator(text);
 
                 while (lineBreakEnumerator.MoveNext())
                 {
                     var currentBreak = lineBreakEnumerator.Current;
 
-                    if (!currentBreak.Required && currentBreak.PositionWrap != text.Length)
+                    if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
                     {
-                        breakOportunities.Enqueue(start + currentBreak.PositionMeasure);
+                        breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
                     }
                 }
+
+                currentPosition += textRun.Length;
             }
 
             if (breakOportunities.Count == 0)
@@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting
             var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
             var spacing = remainingSpace / breakOportunities.Count;
 
+            currentPosition = textLine.FirstTextSourceIndex;
+
             foreach (var textRun in lineImpl.TextRuns)
             {
-                var text = textRun.Text;
+                var text = textRun.CharacterBufferReference.CharacterBuffer;
 
                 if (text.IsEmpty)
                 {
@@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting
                 {
                     var glyphRun = shapedText.GlyphRun;
                     var shapedBuffer = shapedText.ShapedBuffer;
-                    var currentPosition = text.Start;
 
                     while (breakOportunities.Count > 0)
                     {
@@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting
 
                     glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
                 }
+
+                currentPosition += textRun.Length;
             }
         }
     }

+ 8 - 12
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs

@@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public sealed class ShapeableTextCharacters : TextRun
     {
-        public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties, sbyte biDiLevel)
+        public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length,
+            TextRunProperties properties, sbyte biDiLevel)
         {
-            TextSourceLength = text.Length;
-            Text = text;
+            CharacterBufferReference = characterBufferReference;
+            Length = length;
             Properties = properties;
             BidiLevel = biDiLevel;
         }
 
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
 
-        public override ReadOnlySlice<char> Text { get; }
+        public override CharacterBufferReference CharacterBufferReference { get; }
 
         public override TextRunProperties Properties { get; }
-        
+
         public sbyte BidiLevel { get; }
 
         public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters)
         {
-            if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer))
-            {
-                return false;
-            }
-
-            if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start)
+            if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference))
             {
                 return false;
             }

+ 20 - 13
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting
     public sealed class ShapedBuffer : IList<GlyphInfo>
     {
         private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
-
-        public ShapedBuffer(ReadOnlySlice<char> text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
-            : this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
+        
+        public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : 
+            this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface,  fontRenderingEmSize,  bidiLevel)
         {
 
         }
 
-        internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
+        internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
         {
-            Text = text;
+            CharacterBufferRange = characterBufferRange;
             GlyphInfos = glyphInfos;
             GlyphTypeface = glyphTypeface;
             FontRenderingEmSize = fontRenderingEmSize;
@@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting
         }
 
         internal ArraySlice<GlyphInfo> GlyphInfos { get; }
-
-        public ReadOnlySlice<char> Text { get; }
-
+        
         public int Length => GlyphInfos.Length;
 
         public IGlyphTypeface GlyphTypeface { get; }
@@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting
 
         public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
 
+        public CharacterBufferRange CharacterBufferRange { get; }
+        
         /// <summary>
         /// Finds a glyph index for given character index.
         /// </summary>
@@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>The split result.</returns>
         internal SplitResult<ShapedBuffer> Split(int length)
         {
-            if (Text.Length == length)
+            if (CharacterBufferRange.Length == length)
             {
                 return new SplitResult<ShapedBuffer>(this, null);
             }
 
-            var glyphCount = FindGlyphIndex(Text.Start + length);
+            var firstCluster = GlyphClusters[0];
+            var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
+
+            var start = firstCluster < lastCluster ? firstCluster : lastCluster;
+
+            var glyphCount = FindGlyphIndex(start + length);
 
-            var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
+            var first = new ShapedBuffer(CharacterBufferRange.Take(length), 
+                GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
 
-            var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
+            var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
+                GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
 
             return new SplitResult<ShapedBuffer>(first, second);
         }
@@ -255,7 +262,7 @@ namespace Avalonia.Media.TextFormatting
         }
     }
 
-    public readonly struct GlyphInfo
+    public readonly record struct GlyphInfo
     {
         public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default)
         {

+ 9 - 10
src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs

@@ -1,6 +1,5 @@
 using System;
 using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting
         public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
         {
             ShapedBuffer = shapedBuffer;
-            Text = shapedBuffer.Text;
+            CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
+            Length = shapedBuffer.CharacterBufferRange.Length;
             Properties = properties;
-            TextSourceLength = Text.Length;
-            TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize);
+            TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
         }
 
         public bool IsReversed { get; private set; }
@@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting
         public ShapedBuffer ShapedBuffer { get; }
 
         /// <inheritdoc/>
-        public override ReadOnlySlice<char> Text { get; }
+        public override CharacterBufferReference CharacterBufferReference { get; }
 
         /// <inheritdoc/>
         public override TextRunProperties Properties { get; }
 
         /// <inheritdoc/>
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
 
         public TextMetrics TextMetrics { get; }
 
@@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting
 
             #if DEBUG
 
-            if (first.Text.Length != length)
+            if (first.Length != length)
             {
                 throw new InvalidOperationException("Split length mismatch.");
             }
-            
-            #endif
+
+#endif
 
             var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
 
@@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
             return new GlyphRun(
                 ShapedBuffer.GlyphTypeface,
                 ShapedBuffer.FontRenderingEmSize,
-                Text,
+                new CharacterBufferRange(CharacterBufferReference, Length),
                 ShapedBuffer.GlyphIndices,
                 ShapedBuffer.GlyphAdvances,
                 ShapedBuffer.GlyphOffsets,

+ 3 - 1
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@@ -1,6 +1,8 @@
 namespace Avalonia.Media.TextFormatting
 {
-    internal readonly struct SplitResult<T>
+#pragma warning disable CA1815 // Override equals and operator equals on value types
+    public readonly struct SplitResult<T>
+#pragma warning restore CA1815 // Override equals and operator equals on value types
     {
         public SplitResult(T first, T? second)
         {

+ 92 - 37
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -10,26 +9,83 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public class TextCharacters : TextRun
     {
-        public TextCharacters(ReadOnlySlice<char> text, TextRunProperties properties)
-        {
-            TextSourceLength = text.Length;
-            Text = text;
-            Properties = properties;
-        }
+        /// <summary>
+        /// Construct a run of text content from character array
+        /// </summary>
+        public TextCharacters(
+            char[] characterArray,
+            int offsetToFirstChar,
+            int length,
+            TextRunProperties textRunProperties
+            ) :
+            this(
+                new CharacterBufferReference(characterArray, offsetToFirstChar),
+                length,
+                textRunProperties
+                )
+        { }
+
 
-        public TextCharacters(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
-            TextRunProperties properties)
+        /// <summary>
+        /// Construct a run for text content from string 
+        /// </summary>
+        public TextCharacters(
+            string characterString,
+            TextRunProperties textRunProperties
+            ) :
+            this(
+                characterString,
+                0,  // offsetToFirstChar
+                (characterString == null) ? 0 : characterString.Length,
+                textRunProperties
+                )
+        { }
+
+        /// <summary>
+        /// Construct a run for text content from string
+        /// </summary>
+        public TextCharacters(
+            string characterString,
+            int offsetToFirstChar,
+            int length,
+            TextRunProperties textRunProperties
+            ) :
+            this(
+                new CharacterBufferReference(characterString, offsetToFirstChar),
+                length,
+                textRunProperties
+                )
+        { }
+
+        /// <summary>
+        /// Internal constructor of TextContent
+        /// </summary>
+        public TextCharacters(
+            CharacterBufferReference characterBufferReference,
+            int length,
+            TextRunProperties textRunProperties
+            )
         {
-            Text = text.Skip(offsetToFirstCharacter).Take(length);
-            TextSourceLength = length;
-            Properties = properties;
+            if (length <= 0)
+            {
+                throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
+            }
+
+            if (textRunProperties.FontRenderingEmSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
+            }
+
+            CharacterBufferReference = characterBufferReference;
+            Length = length;
+            Properties = textRunProperties;
         }
 
         /// <inheritdoc />
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
 
         /// <inheritdoc />
-        public override ReadOnlySlice<char> Text { get; }
+        public override CharacterBufferReference CharacterBufferReference { get; }
 
         /// <inheritdoc />
         public override TextRunProperties Properties { get; }
@@ -38,18 +94,17 @@ namespace Avalonia.Media.TextFormatting
         /// Gets a list of <see cref="ShapeableTextCharacters"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
-            ref TextRunProperties? previousProperties)
+        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var shapeableCharacters = new List<ShapeableTextCharacters>(2);
 
-            while (!runText.IsEmpty)
+            while (characterBufferRange.Length > 0)
             {
-                var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
+                var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
 
                 shapeableCharacters.Add(shapeableRun);
 
-                runText = runText.Skip(shapeableRun.Text.Length);
+                characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
 
                 previousProperties = shapeableRun.Properties;
             }
@@ -60,45 +115,45 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Creates a shapeable text run with unique properties.
         /// </summary>
-        /// <param name="text">The text to create text runs from.</param>
+        /// <param name="characterBufferRange">The character buffer range to create text runs from.</param>
         /// <param name="defaultProperties">The default text run properties.</param>
         /// <param name="biDiLevel">The bidi level of the run.</param>
         /// <param name="previousProperties"></param>
         /// <returns>A list of shapeable text runs.</returns>
-        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
+        private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange,
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var defaultTypeface = defaultProperties.Typeface;
             var currentTypeface = defaultTypeface;
             var previousTypeface = previousProperties?.Typeface;
 
-            if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
+            if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
             {
                 if (script == Script.Common && previousTypeface is not null)
                 {
-                    if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _))
+                    if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
                     {
-                        return new ShapeableTextCharacters(text.Take(fallbackCount),
+                        return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount,
                             defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                     }
                 }
 
-                return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+                return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
 
             if (previousTypeface is not null)
             {
-                if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
+                if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
                 {
-                    return new ShapeableTextCharacters(text.Take(count),
+                    return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count,
                         defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                 }
             }
 
             var codepoint = Codepoint.ReplacementCodepoint;
 
-            var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
+            var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
 
             while (codepointEnumerator.MoveNext())
             {
@@ -118,10 +173,10 @@ namespace Avalonia.Media.TextFormatting
                     defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
                     out currentTypeface);
 
-            if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
+            if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
             {
                 //Fallback found
-                return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+                return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
 
@@ -130,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
 
             var glyphTypeface = currentTypeface.GlyphTypeface;
 
-            var enumerator = new GraphemeEnumerator(text);
+            var enumerator = new GraphemeEnumerator(characterBufferRange);
 
             while (enumerator.MoveNext())
             {
@@ -144,20 +199,20 @@ namespace Avalonia.Media.TextFormatting
                 count += grapheme.Text.Length;
             }
 
-            return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
+            return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
         }
 
         /// <summary>
         /// Tries to get a shapeable length that is supported by the specified typeface.
         /// </summary>
-        /// <param name="text">The text.</param>
+        /// <param name="characterBufferRange">The character buffer range to shape.</param>
         /// <param name="typeface">The typeface that is used to find matching characters.</param>
         /// <param name="defaultTypeface"></param>
         /// <param name="length">The shapeable length.</param>
         /// <param name="script"></param>
         /// <returns></returns>
-        protected static bool TryGetShapeableLength(
-            ReadOnlySlice<char> text,
+        internal static bool TryGetShapeableLength(
+            CharacterBufferRange characterBufferRange,
             Typeface typeface,
             Typeface? defaultTypeface,
             out int length,
@@ -166,7 +221,7 @@ namespace Avalonia.Media.TextFormatting
             length = 0;
             script = Script.Unknown;
 
-            if (text.Length == 0)
+            if (characterBufferRange.Length == 0)
             {
                 return false;
             }
@@ -174,7 +229,7 @@ namespace Avalonia.Media.TextFormatting
             var font = typeface.GlyphTypeface;
             var defaultFont = defaultTypeface?.GlyphTypeface;
 
-            var enumerator = new GraphemeEnumerator(text);
+            var enumerator = new GraphemeEnumerator(characterBufferRange);
 
             while (enumerator.MoveNext())
             {

+ 52 - 50
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case ShapedTextCharacters shapedRun:
-                    {
-                        currentWidth += shapedRun.Size.Width;
-
-                        if (currentWidth > availableWidth)
                         {
-                            if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+                            currentWidth += shapedRun.Size.Width;
+
+                            if (currentWidth > availableWidth)
                             {
-                                if (isWordEllipsis && measuredLength < textLine.Length)
+                                if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
                                 {
-                                    var currentBreakPosition = 0;
+                                    if (isWordEllipsis && measuredLength < textLine.Length)
+                                    {
+                                        var currentBreakPosition = 0;
 
-                                    var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+                                        var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
 
-                                    while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
-                                    {
-                                        var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+                                        var lineBreaker = new LineBreakEnumerator(text);
 
-                                        if (nextBreakPosition == 0)
+                                        while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
                                         {
-                                            break;
-                                        }
+                                            var nextBreakPosition = lineBreaker.Current.PositionMeasure;
 
-                                        if (nextBreakPosition >= measuredLength)
-                                        {
-                                            break;
+                                            if (nextBreakPosition == 0)
+                                            {
+                                                break;
+                                            }
+
+                                            if (nextBreakPosition >= measuredLength)
+                                            {
+                                                break;
+                                            }
+
+                                            currentBreakPosition = nextBreakPosition;
                                         }
 
-                                        currentBreakPosition = nextBreakPosition;
+                                        measuredLength = currentBreakPosition;
                                     }
-
-                                    measuredLength = currentBreakPosition;
                                 }
-                            }
 
-                            collapsedLength += measuredLength;
+                                collapsedLength += measuredLength;
 
-                            var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
 
-                            if (collapsedLength > 0)
-                            {
-                                var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                if (collapsedLength > 0)
+                                {
+                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
 
-                                collapsedRuns.AddRange(splitResult.First);
-                            }
+                                    collapsedRuns.AddRange(splitResult.First);
+                                }
 
-                            collapsedRuns.Add(shapedSymbol);
+                                collapsedRuns.Add(shapedSymbol);
 
-                            return collapsedRuns;
-                        }
+                                return collapsedRuns;
+                            }
 
-                        availableWidth -= currentRun.Size.Width;
+                            availableWidth -= currentRun.Size.Width;
 
-                        
-                        break;
-                    }
+
+                            break;
+                        }
 
                     case { } drawableRun:
-                    {
-                        //The whole run needs to fit into available space
-                        if (currentWidth + drawableRun.Size.Width > availableWidth)
                         {
-                            var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
-
-                            if (collapsedLength > 0)
+                            //The whole run needs to fit into available space
+                            if (currentWidth + drawableRun.Size.Width > availableWidth)
                             {
-                                var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
 
-                                collapsedRuns.AddRange(splitResult.First);
-                            }
+                                if (collapsedLength > 0)
+                                {
+                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+
+                                    collapsedRuns.AddRange(splitResult.First);
+                                }
+
+                                collapsedRuns.Add(shapedSymbol);
 
-                            collapsedRuns.Add(shapedSymbol);
+                                return collapsedRuns;
+                            }
 
-                            return collapsedRuns;
+                            break;
                         }
-                        
-                        break;
-                    }
                 }
 
-                collapsedLength += currentRun.TextSourceLength;
+                collapsedLength += currentRun.Length;
 
                 runIndex++;
             }

+ 2 - 2
src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs

@@ -7,9 +7,9 @@
     {
         public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
         {
-            TextSourceLength = textSourceLength;
+            Length = textSourceLength;
         }
 
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
     }
 }

+ 52 - 48
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = textRuns[i];
 
-                if (currentLength + currentRun.TextSourceLength < length)
+                if (currentLength + currentRun.Length < length)
                 {
-                    currentLength += currentRun.TextSourceLength;
+                    currentLength += currentRun.Length;
 
                     continue;
                 }
 
-                var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
+                var firstCount = currentRun.Length >= 1 ? i + 1 : i;
 
                 var first = new List<DrawableTextRun>(firstCount);
 
@@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
 
                 var secondCount = textRuns.Count - firstCount;
 
-                if (currentLength + currentRun.TextSourceLength == length)
+                if (currentLength + currentRun.Length == length)
                 {
                     var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
 
                     if (second != null)
                     {
-                        var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
+                        var offset = currentRun.Length >= 1 ? 1 : 0;
 
                         for (var j = 0; j < secondCount; j++)
                         {
@@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textRun in textRuns)
             {
-                if (textRun.Text.IsEmpty)
+                if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
                 {
-                    var text = new char[textRun.TextSourceLength];
+                    var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
 
-                    biDiData.Append(text);
+                    biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
                 }
                 else
                 {
-                    biDiData.Append(textRun.Text);
+                    var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
+
+                    biDiData.Append(text);
                 }
             }
 
@@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting
                     case ShapeableTextCharacters shapeableRun:
                         {
                             var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
-                            var text = currentRun.Text;
-                            var start = currentRun.Text.Start;
-                            var length = currentRun.Text.Length;
-                            var bufferOffset = currentRun.Text.BufferOffset;
+                            var characterBufferReference = currentRun.CharacterBufferReference;
+                            var length = currentRun.Length;
+                            var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
 
                             while (index + 1 < processedRuns.Count)
                             {
@@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting
                                 {
                                     groupedRuns.Add(nextRun);
 
-                                    length += nextRun.Text.Length;
-
-                                    if (start > nextRun.Text.Start)
-                                    {
-                                        start = nextRun.Text.Start;
-                                    }
+                                    length += nextRun.Length;
 
-                                    if (bufferOffset > nextRun.Text.BufferOffset)
+                                    if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
                                     {
-                                        bufferOffset = nextRun.Text.BufferOffset;
+                                        offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
                                     }
 
-                                    text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
+                                    characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
 
                                     index++;
 
@@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting
                                          shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, 
                                          paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
 
-                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
 
                             break;
                         }
@@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
         }
 
         private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
-            IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
+            IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
         {
             var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
 
-            var shapedBuffer = TextShaper.Current.ShapeText(text, options);
+            var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
 
             for (var i = 0; i < textRuns.Count; i++)
             {
                 var currentRun = textRuns[i];
 
-                var splitResult = shapedBuffer.Split(currentRun.Text.Length);
+                var splitResult = shapedBuffer.Split(currentRun.Length);
 
                 shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
 
@@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting
 
             TextRunProperties? previousProperties = null;
             TextCharacters? currentRun = null;
-            var runText = ReadOnlySlice<char>.Empty;
+            CharacterBufferRange runText = default;
 
             for (var i = 0; i < textCharacters.Count; i++)
             {
@@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting
 
                     yield return new[] { drawableRun };
 
-                    levelIndex += drawableRun.TextSourceLength;
+                    levelIndex += drawableRun.Length;
 
                     continue;
                 }
 
-                runText = currentRun.Text;
+                runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
 
                 for (; j < runText.Length;)
                 {
@@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     endOfLine = textEndOfLine;
 
-                    textSourceLength += textEndOfLine.TextSourceLength;
+                    textSourceLength += textEndOfLine.Length;
 
                     textRuns.Add(textRun);
 
@@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (TryGetLineBreak(textCharacters, out var runLineBreak))
                             {
-                                var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
+                                var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
                                     textCharacters.Properties);
 
                                 textRuns.Add(splitResult);
@@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                 }
 
-                textSourceLength += textRun.TextSourceLength;
+                textSourceLength += textRun.Length;
             }
 
             return textRuns;
@@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting
         {
             lineBreak = default;
 
-            if (textRun.Text.IsEmpty)
+            if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
             {
                 return false;
             }
 
-            var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
+            var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
+
+            var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
 
             while (lineBreakEnumerator.MoveNext())
             {
@@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
 
                 lineBreak = lineBreakEnumerator.Current;
 
-                return lineBreak.PositionWrap >= textRun.Text.Length || true;
+                return lineBreak.PositionWrap >= textRun.Length || true;
             }
 
             return false;
@@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if(shapedTextCharacters.ShapedBuffer.Length > 0)
                             {
-                                var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
+                                var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
                                 var lastCluster = firstCluster;
 
                                 for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
@@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting
                                     currentWidth += glyphInfo.GlyphAdvance;
                                 }
 
-                                measuredLength += currentRun.TextSourceLength;
+                                measuredLength += currentRun.Length;
                             }                         
 
                             break;
@@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting
                                 goto found;
                             }
 
-                            measuredLength += currentRun.TextSourceLength;
+                            measuredLength += currentRun.Length;
                             currentWidth += currentRun.Size.Width;
 
                             break;
@@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting
             var flowDirection = paragraphProperties.FlowDirection;
             var properties = paragraphProperties.DefaultTextRunProperties;
             var glyphTypeface = properties.Typeface.GlyphTypeface;
-            var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
             var glyph = glyphTypeface.GetGlyph(s_empty[0]);
             var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
 
-            var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
+            var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
+            var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
                 (sbyte)flowDirection);
 
             var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
@@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = textRuns[index];
 
-                var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+                var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+
+                var lineBreaker = new LineBreakEnumerator(runText);
 
                 var breakFound = false;
 
@@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
                             //Find next possible wrap position (overflow)
                             if (index < textRuns.Count - 1)
                             {
-                                if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
+                                if (lineBreaker.Current.PositionWrap != currentRun.Length)
                                 {
                                     //We already found the next possible wrap position.
                                     breakFound = true;
@@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting
                                 {
                                     currentPosition += lineBreaker.Current.PositionWrap;
 
-                                    if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
+                                    if (lineBreaker.Current.PositionWrap != currentRun.Length)
                                     {
                                         break;
                                     }
@@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting
 
                                     currentRun = textRuns[index];
 
-                                    lineBreaker = new LineBreakEnumerator(currentRun.Text);
+                                    runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+
+                                    lineBreaker = new LineBreakEnumerator(runText);
                                 }
                             }
                             else
@@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (!breakFound)
                 {
-                    currentLength += currentRun.TextSourceLength;
+                    currentLength += currentRun.Length;
 
                     continue;
                 }
@@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting
                     return false;
                 }
 
-                if (Current.TextSourceLength == 0)
+                if (Current.Length == 0)
                 {
                     return false;
                 }
 
-                _pos += Current.TextSourceLength;
+                _pos += Current.Length;
 
                 return true;
             }
@@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting
 
             var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
 
-            var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
+            var characterBuffer = textRun.CharacterBufferReference;
+
+            var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
 
             return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
         }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting
                 CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
                     textDecorations, flowDirection, lineHeight, letterSpacing);
 
-            _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
+            _textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
 
             _textTrimming = textTrimming ?? TextTrimming.None;
 

+ 2 - 3
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="width">width in which collapsing is constrained to</param>
         /// <param name="textRunProperties">text run properties of ellipsis symbol</param>
         public TextLeadingPrefixCharacterEllipsis(
-            ReadOnlySlice<char> ellipsis,
+            string ellipsis,
             int prefixLength,
             double width,
             TextRunProperties textRunProperties)
@@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
                                                 if (suffixCount > 0)
                                                 {
                                                     var splitSuffix =
-                                                        endShapedRun.Split(run.TextSourceLength - suffixCount);
+                                                        endShapedRun.Split(run.Length - suffixCount);
 
                                                     collapsedRuns.Add(splitSuffix.Second!);
                                                 }

+ 177 - 181
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting
         public override double Height => _textLineMetrics.Height;
 
         /// <inheritdoc/>
-        public override int NewLineLength => _textLineMetrics.NewLineLength;
+        public override int NewLineLength => _textLineMetrics.NewlineLength;
 
         /// <inheritdoc/>
         public override double OverhangAfter => 0;
@@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 var lastRun = _textRuns[_textRuns.Count - 1];
 
-                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
+                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
             }
 
             // process hit that happens within the line
@@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting
                 if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
                 {
                     var rightToLeftIndex = i;
-                    currentPosition += currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
 
                     while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
                     {
-                        var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
+                        var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
 
                         if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
                         {
                             break;
                         }
 
-                        currentPosition += nextShaped.TextSourceLength;
+                        currentPosition += nextShaped.Length;
 
                         rightToLeftIndex++;
                     }
@@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting
                         if (currentDistance + currentRun.Size.Width <= distance)
                         {
                             currentDistance += currentRun.Size.Width;
-                            currentPosition -= currentRun.TextSourceLength;
+                            currentPosition -= currentRun.Length;
 
                             continue;
                         }
 
-                        characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
-
-                        break;
+                        return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
                     }
                 }
 
-                if (currentDistance + currentRun.Size.Width < distance)
+                characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
+
+                if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
                 {
                     currentDistance += currentRun.Size.Width;
-                    currentPosition += currentRun.TextSourceLength;
+
+                    currentPosition += currentRun.Length;
 
                     continue;
                 }
 
-                characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
-
                 break;
             }
 
@@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting
 
                         if (shapedRun.GlyphRun.IsLeftToRight)
                         {
-                            offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
+                            offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
                         }
 
-                        characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
+                        characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength);
 
                         break;
                     }
@@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                         else
                         {
-                            characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
+                            characterHit = new CharacterHit(currentPosition, run.Length);
                         }
                         break;
                     }
@@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting
 
                                 rightToLeftWidth -= currentRun.Size.Width;
 
-                                if (currentPosition + currentRun.TextSourceLength >= characterIndex)
+                                if (currentPosition + currentRun.Length >= characterIndex)
                                 {
                                     break;
                                 }
 
-                                currentPosition += currentRun.TextSourceLength;
+                                currentPosition += currentRun.Length;
 
-                                remainingLength -= currentRun.TextSourceLength;
+                                remainingLength -= currentRun.Length;
 
                                 i--;
                             }
@@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                     }
 
-                    if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
+                    if (currentPosition + currentRun.Length >= characterIndex &&
                         TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
                     {
                         return Math.Max(0, currentDistance + distance);
@@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting
 
                     //No hit hit found so we add the full width
                     currentDistance += currentRun.Size.Width;
-                    currentPosition += currentRun.TextSourceLength;
-                    remainingLength -= currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
+                    remainingLength -= currentRun.Length;
                 }
             }
             else
@@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting
 
                     //No hit hit found so we add the full width
                     currentDistance -= currentRun.Size.Width;
-                    currentPosition += currentRun.TextSourceLength;
-                    remainingLength -= currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
+                    remainingLength -= currentRun.Length;
                 }
             }
 
@@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting
                     {
                         currentGlyphRun = shapedTextCharacters.GlyphRun;
 
-                        if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
+                        if (currentPosition + remainingLength <= currentPosition + currentRun.Length)
                         {
-                            characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
+                            characterHit = new CharacterHit(currentPosition + remainingLength);
 
                             distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
 
                             return true;
                         }
 
-                        if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit)
+                        if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit)
                         {
                             if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
                             {
@@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting
                             return true;
                         }
 
-                        if (characterIndex == currentPosition + currentRun.TextSourceLength)
+                        if (characterIndex == currentPosition + currentRun.Length)
                         {
                             distance = currentRun.Size.Width;
 
@@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting
             {
                 case ShapedTextCharacters shapedRun:
                     {
-                        characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+                        nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
                         break;
                     }
                 default:
                     {
-                        characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+                        nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
                         break;
                     }
             }
 
-            return characterHit;
+            if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
+            {
+                return characterHit;
+            }
+
+            return nextCharacterHit;
         }
 
         /// <inheritdoc/>
@@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting
                 var characterLength = 0;
                 var endX = startX;
 
-                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)
+                if (currentRun is ShapedTextCharacters currentShapedRun)
                 {
-                    var rightToLeftIndex = index;
-                    var rightToLeftWidth = currentShapedRun.Size.Width;
+                    var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
 
-                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
+                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
                     {
-                        if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
-                        {
-                            break;
-                        }
+                        startX += currentRun.Size.Width;
 
-                        rightToLeftIndex++;
+                        currentPosition += currentRun.Length;
 
-                        rightToLeftWidth += nextShapedRun.Size.Width;
-
-                        if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
-                        {
-                            break;
-                        }
-
-                        currentShapedRun = nextShapedRun;
+                        continue;
                     }
 
-                    startX = startX + rightToLeftWidth;
+                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                    {
+                        var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
 
-                    currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+                        double startOffset;
 
-                    remainingLength -= currentRunBounds.Length;
-                    currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
-                    endX = currentRunBounds.Rectangle.Right;
-                    startX = currentRunBounds.Rectangle.Left;
+                        double endOffset;
 
-                    var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
+                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
 
-                    for (int i = rightToLeftIndex - 1; i >= index; i--)
-                    {
-                        currentShapedRun = TextRuns[i] as ShapedTextCharacters;
+                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
 
-                        if(currentShapedRun == null)
-                        {
-                            continue;
-                        }
+                        startX += startOffset;
 
-                        currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+                        endX += endOffset;
 
-                        rightToLeftRunBounds.Insert(0, currentRunBounds);
+                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
 
-                        remainingLength -= currentRunBounds.Length;
-                        startX = currentRunBounds.Rectangle.Left;
+                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
 
-                        currentPosition += currentRunBounds.Length;
+                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+
+                        currentDirection = FlowDirection.LeftToRight;
                     }
+                    else
+                    {
+                        var rightToLeftIndex = index;
+                        var rightToLeftWidth = currentShapedRun.Size.Width;
 
-                    combinedWidth = endX - startX;
+                        while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
+                        {
+                            if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+                            {
+                                break;
+                            }
 
-                    currentRect = new Rect(startX, 0, combinedWidth, Height);
+                            rightToLeftIndex++;
 
-                    currentDirection = FlowDirection.RightToLeft;
+                            rightToLeftWidth += nextShapedRun.Size.Width;
 
-                    if (!MathUtilities.IsZero(combinedWidth))
-                    {
-                        result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
-                    }
+                            if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
+                            {
+                                break;
+                            }
 
-                    startX = endX;
-                }
-                else
-                {
-                    if (currentShapedRun != null)
-                    {
-                        var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+                            currentShapedRun = nextShapedRun;
+                        }
 
-                        currentPosition += offset;
+                        startX += rightToLeftWidth;
 
-                        var startIndex = currentRun.Text.Start + offset;
+                        currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
 
-                        double startOffset;
-                        double endOffset;
+                        remainingLength -= currentRunBounds.Length;
+                        currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
+                        endX = currentRunBounds.Rectangle.Right;
+                        startX = currentRunBounds.Rectangle.Left;
 
-                        if (currentShapedRun.ShapedBuffer.IsLeftToRight)
-                        {
-                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
 
-                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
-                        }
-                        else
+                        for (int i = rightToLeftIndex - 1; i >= index; i--)
                         {
-                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-
-                            if (currentPosition < startIndex)
-                            {
-                                startOffset = endOffset;
-                            }
-                            else
+                            if (TextRuns[i] is not ShapedTextCharacters)
                             {
-                                startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                                continue;
                             }
-                        }
 
-                        startX += startOffset;
+                            currentShapedRun = (ShapedTextCharacters)TextRuns[i];
 
-                        endX += endOffset;
+                            currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
 
-                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+                            rightToLeftRunBounds.Insert(0, currentRunBounds);
 
-                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+                            remainingLength -= currentRunBounds.Length;
+                            startX = currentRunBounds.Rectangle.Left;
 
-                        currentDirection = FlowDirection.LeftToRight;
-                    }
-                    else
-                    {
-                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                        {
-                            startX += currentRun.Size.Width;
+                            currentPosition += currentRunBounds.Length;
+                        }
 
-                            currentPosition += currentRun.TextSourceLength;
+                        combinedWidth = endX - startX;
 
-                            continue;
-                        }
+                        currentRect = new Rect(startX, 0, combinedWidth, Height);
+
+                        currentDirection = FlowDirection.RightToLeft;
 
-                        if (currentPosition < firstTextSourceIndex)
+                        if (!MathUtilities.IsZero(combinedWidth))
                         {
-                            startX += currentRun.Size.Width;
+                            result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
                         }
 
-                        if (currentPosition + currentRun.TextSourceLength <= characterIndex)
-                        {
-                            endX += currentRun.Size.Width;
+                        startX = endX;
+                    }
+                }
+                else
+                {
+                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+                    {
+                        startX += currentRun.Size.Width;
 
-                            characterLength = currentRun.TextSourceLength;
-                        }
+                        currentPosition += currentRun.Length;
+
+                        continue;
                     }
 
-                    if (endX < startX)
+                    if (currentPosition < firstTextSourceIndex)
                     {
-                        (endX, startX) = (startX, endX);
+                        startX += currentRun.Size.Width;
                     }
 
-                    //Lines that only contain a linebreak need to be covered here
-                    if (characterLength == 0)
+                    if (currentPosition + currentRun.Length <= characterIndex)
                     {
-                        characterLength = NewLineLength;
+                        endX += currentRun.Size.Width;
+
+                        characterLength = currentRun.Length;
                     }
+                }
 
-                    combinedWidth = endX - startX;
+                if (endX < startX)
+                {
+                    (endX, startX) = (startX, endX);
+                }
 
-                    currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
+                //Lines that only contain a linebreak need to be covered here
+                if (characterLength == 0)
+                {
+                    characterLength = NewLineLength;
+                }
 
-                    currentPosition += characterLength;
+                combinedWidth = endX - startX;
 
-                    remainingLength -= characterLength;
+                currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
 
-                    startX = endX;
+                currentPosition += characterLength;
 
-                    if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
-                    {
-                        if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
-                        {
-                            currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
+                remainingLength -= characterLength;
 
-                            var textBounds = result[result.Count - 1];
+                startX = endX;
 
-                            textBounds.Rectangle = currentRect;
+                if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
+                {
+                    if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+                    {
+                        currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
 
-                            textBounds.TextRunBounds.Add(currentRunBounds);
-                        }
-                        else
-                        {
-                            currentRect = currentRunBounds.Rectangle;
+                        var textBounds = result[result.Count - 1];
 
-                            result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
-                        }
+                        textBounds.Rectangle = currentRect;
+
+                        textBounds.TextRunBounds.Add(currentRunBounds);
                     }
+                    else
+                    {
+                        currentRect = currentRunBounds.Rectangle;
 
-                    lastRunBounds = currentRunBounds;
+                        result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
+                    }
                 }
 
+                lastRunBounds = currentRunBounds;
+
                 currentWidth += combinedWidth;
 
                 if (remainingLength <= 0 || currentPosition >= characterIndex)
@@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
+                if (currentPosition + currentRun.Length < firstTextSourceIndex)
                 {
                     startX -= currentRun.Size.Width;
 
-                    currentPosition += currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
 
                     continue;
                 }
@@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting
 
                     currentPosition += offset;
 
-                    var startIndex = currentRun.Text.Start + offset;
+                    var startIndex = currentPosition;
                     double startOffset;
                     double endOffset;
 
@@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting
                 }
                 else
                 {
-                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                    if (currentPosition + currentRun.Length <= characterIndex)
                     {
                         endX -= currentRun.Size.Width;
                     }
@@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting
                     {
                         startX -= currentRun.Size.Width;
 
-                        characterLength = currentRun.TextSourceLength;
+                        characterLength = currentRun.Length;
                     }
                 }
 
@@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting
 
             currentPosition += offset;
 
-            var startIndex = currentRun.Text.Start + offset;
+            var startIndex = currentPosition;
 
             double startOffset;
             double endOffset;
@@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting
                                 return true;
                             }
 
-                            var characterIndex = codepointIndex - shapedRun.Text.Start;
+                            //var characterIndex = codepointIndex - shapedRun.Text.Start;
 
-                            if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
-                            {
-                                foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
-                            }
+                            //if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
+                            //{
+                            //    foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+                            //}
 
                             nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
                                 foundCharacterHit :
@@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting
 
                             if (textPosition == currentPosition)
                             {
-                                nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+                                nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
 
                                 return true;
                             }
@@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                 }
 
-                currentPosition += currentRun.TextSourceLength;
+                currentPosition += currentRun.Length;
                 runIndex++;
             }
 
@@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                     default:
                         {
-                            if (characterIndex == currentPosition + currentRun.TextSourceLength)
+                            if (characterIndex == currentPosition + currentRun.Length)
                             {
                                 previousCharacterHit = new CharacterHit(currentPosition);
 
@@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                 }
 
-                currentPosition -= currentRun.TextSourceLength;
+                currentPosition -= currentRun.Length;
                 runIndex--;
             }
 
@@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextCharacters shapedRun:
                         {
+                            var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
+
+                            if (firstCluster > codepointIndex)
+                            {
+                                break;
+                            }
+
                             if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
                             {
                                 if (shapedRun.ShapedBuffer.IsLeftToRight)
                                 {
-                                    if (currentRun.Text.Start >= codepointIndex)
+                                    if (firstCluster >= codepointIndex)
                                     {
                                         return --runIndex;
                                     }
                                 }
                                 else
                                 {
-                                    if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+                                    if (codepointIndex > firstCluster + currentRun.Length)
                                     {
                                         return --runIndex;
                                     }
@@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting
 
                             if (direction == LogicalDirection.Forward)
                             {
-                                if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
+                                if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length)
                                 {
                                     return runIndex;
                                 }
                             }
                             else
                             {
-                                if (codepointIndex > currentRun.Text.Start &&
-                                    codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
+                                if (codepointIndex > firstCluster &&
+                                    codepointIndex <= firstCluster + currentRun.Length)
                                 {
                                     return runIndex;
                                 }
@@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting
                                 return runIndex;
                             }
 
+                            textPosition += currentRun.Length;
+
                             break;
                         }
 
@@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting
                                 return runIndex;
                             }
 
+                            textPosition += currentRun.Length;
+
                             break;
                         }
                 }
 
                 runIndex++;
                 previousRun = currentRun;
-                textPosition += currentRun.TextSourceLength;
             }
 
             return runIndex;
@@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting
                     case ShapedTextCharacters textRun:
                         {
                             var textMetrics =
-                                new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+                                new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
 
                             if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
                             {
@@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
                                 trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
-                                newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+                                newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
                             }
 
                             widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;

+ 4 - 4
src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs

@@ -4,15 +4,15 @@
     /// Represents a metric for a <see cref="TextLine"/> objects,
     /// that holds information about ascent, descent, line gap, size and origin of the text line.
     /// </summary>
-    public readonly struct TextLineMetrics
+    public readonly record struct TextLineMetrics
     {
-        public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
+        public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
             int trailingWhitespaceLength, double width,
             double widthIncludingTrailingWhitespace)
         {
             HasOverflowed = hasOverflowed;
             Height = height;
-            NewLineLength = newLineLength;
+            NewlineLength = newlineLength;
             Start = start;
             TextBaseline = textBaseline;
             TrailingWhitespaceLength = trailingWhitespaceLength;
@@ -33,7 +33,7 @@
         /// <summary>
         /// Gets the number of newline characters at the end of a line.
         /// </summary>
-        public int NewLineLength { get; }
+        public int NewlineLength { get; }
         
         /// <summary>
         /// Gets the distance from the start of a paragraph to the starting point of a line.

+ 3 - 3
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@@ -3,11 +3,11 @@
     /// <summary>
     /// A metric that holds information about text specific measurements.
     /// </summary>
-    public readonly struct TextMetrics
+    public readonly record struct TextMetrics
     {
-        public TextMetrics(Typeface typeface, double fontRenderingEmSize)
+        public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
         {
-            var fontMetrics = typeface.GlyphTypeface.Metrics;
+            var fontMetrics = glyphTypeface.Metrics;
 
             var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
 

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextRange.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
     /// <summary>
     /// References a portion of a text buffer.
     /// </summary>
-    public readonly struct TextRange
+    public readonly record struct TextRange
     {
         public TextRange(int start, int length)
         {

+ 6 - 5
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@@ -1,5 +1,4 @@
 using System.Diagnostics;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         ///  Gets the text source length.
         /// </summary>
-        public virtual int TextSourceLength => DefaultTextSourceLength;
+        public virtual int Length => DefaultTextSourceLength;
 
         /// <summary>
         /// Gets the text run's text.
         /// </summary>
-        public virtual ReadOnlySlice<char> Text => default;
+        public virtual CharacterBufferReference CharacterBufferReference => default;
 
         /// <summary>
         /// A set of properties shared by every characters in the run
@@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting
                 {
                     unsafe
                     {
-                        fixed (char* charsPtr = _textRun.Text.Span)
+                        var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
+
+                        fixed (char* charsPtr = characterBuffer.Span)
                         {
-                            return new string(charsPtr, 0, _textRun.Text.Length);
+                            return new string(charsPtr, 0, characterBuffer.Span.Length);
                         }
                     }
                 }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs

@@ -3,7 +3,7 @@
     /// <summary>
     /// The bounding rectangle of text run
     /// </summary>
-    public readonly struct TextRunBounds
+    public readonly record struct TextRunBounds
     {
         /// <summary>
         /// Constructing TextRunBounds

+ 7 - 4
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@@ -1,7 +1,5 @@
 using System;
-using System.Globalization;
 using Avalonia.Platform;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting
         }
 
         /// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
-        public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
+        public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
         {
-            return _platformImpl.ShapeText(text, options);
+            return _platformImpl.ShapeText(text, length, options);
+        }
+
+        public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
+        {
+            return ShapeText(new CharacterBufferReference(text), text.Length, options);
         }
     }
 }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
     /// <summary>
     /// Options to customize text shaping.
     /// </summary>
-    public readonly struct TextShaperOptions
+    public readonly record struct TextShaperOptions
     {
         public TextShaperOptions(
             IGlyphTypeface typeface, 

+ 1 - 2
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="ellipsis">Text used as collapsing symbol.</param>
         /// <param name="width">Width in which collapsing is constrained to.</param>
         /// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
-        public TextTrailingCharacterEllipsis(ReadOnlySlice<char> ellipsis, double width, TextRunProperties textRunProperties)
+        public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
         {
             Width = width;
             Symbol = new TextCharacters(ellipsis, textRunProperties);

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="width">width in which collapsing is constrained to.</param>
         /// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
         public TextTrailingWordEllipsis(
-            ReadOnlySlice<char> ellipsis,
+            string ellipsis,
             double width,
             TextRunProperties textRunProperties
         )

+ 2 - 1
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0.
 // Ported from: https://github.com/SixLabors/Fonts/
 
+using System;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
@@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// Appends text to the bidi data.
         /// </summary>
         /// <param name="text">The text to process.</param>
-        public void Append(ReadOnlySlice<char> text)
+        public void Append(CharacterBufferRange text)
         {
             _classes.Add(text.Length);
             _pairedBracketTypes.Add(text.Length);

+ 5 - 6
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@@ -1,10 +1,9 @@
-using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
-    public readonly struct Codepoint
+    public readonly record struct Codepoint
     {
         private readonly uint _value;
 
@@ -166,11 +165,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <param name="index">The index to read at.</param>
         /// <param name="count">The count of character that were read.</param>
         /// <returns></returns>
-        public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
+        public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
         {
             count = 1;
 
-            if (index >= text.Length)
+            if (index >= text.Count)
             {
                 return ReplacementCodepoint;
             }
@@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
             {
                 hi = code;
 
-                if (index + 1 == text.Length)
+                if (index + 1 == text.Count)
                 {
                     return ReplacementCodepoint;
                 }

+ 4 - 3
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@@ -1,12 +1,13 @@
-using Avalonia.Utilities;
+using System;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     public ref struct CodepointEnumerator
     {
-        private ReadOnlySlice<char> _text;
+        private CharacterBufferRange _text;
+        private int _pos;
 
-        public CodepointEnumerator(ReadOnlySlice<char> text)
+        public CodepointEnumerator(CharacterBufferRange text)
         {
             _text = text;
             Current = Codepoint.ReplacementCodepoint;

+ 4 - 4
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@@ -1,13 +1,13 @@
-using Avalonia.Utilities;
+using System;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     /// <summary>
     /// Represents the smallest unit of a writing system of any given language.
     /// </summary>
-    public readonly struct Grapheme
+    public readonly ref struct Grapheme
     {
-        public Grapheme(Codepoint firstCodepoint, ReadOnlySlice<char> text)
+        public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
         {
             FirstCodepoint = firstCodepoint;
             Text = text;
@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <summary>
         /// The text that is representing the <see cref="Grapheme"/>.
         /// </summary>
-        public ReadOnlySlice<char> Text { get; }
+        public ReadOnlySpan<char> Text { get; }
     }
 }

+ 6 - 6
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@@ -3,16 +3,16 @@
 // 
 // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
+using System.Collections.Generic;
 using System.Runtime.InteropServices;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     public ref struct GraphemeEnumerator
     {
-        private ReadOnlySlice<char> _text;
+        private CharacterBufferRange _text;
 
-        public GraphemeEnumerator(ReadOnlySlice<char> text)
+        public GraphemeEnumerator(CharacterBufferRange text)
         {
             _text = text;
             Current = default;
@@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
             var text = _text.Take(processor.CurrentCodeUnitOffset);
 
-            Current = new Grapheme(firstCodepoint, text);
+            Current = new Grapheme(firstCodepoint, text.Span);
 
             _text = _text.Skip(processor.CurrentCodeUnitOffset);
 
@@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
         [StructLayout(LayoutKind.Auto)]
         private ref struct Processor
         {
-            private readonly ReadOnlySlice<char> _buffer;
+            private readonly CharacterBufferRange _buffer;
             private int _codeUnitLengthOfCurrentScalar;
 
-            internal Processor(ReadOnlySlice<char> buffer)
+            internal Processor(CharacterBufferRange buffer)
             {
                 _buffer = buffer;
                 _codeUnitLengthOfCurrentScalar = 0;

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
     /// Information about a potential line break position
     /// </summary>
     [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")]
-    public readonly struct LineBreak
+    public readonly record struct LineBreak
     {
         /// <summary>
         /// Constructor

+ 8 - 7
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@@ -2,7 +2,8 @@
 // Licensed under the Apache License, Version 2.0.
 // Ported from: https://github.com/SixLabors/Fonts/
 
-using Avalonia.Utilities;
+using System;
+using System.Collections.Generic;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
@@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
     /// </summary>
     public ref struct LineBreakEnumerator
     {
-        private readonly ReadOnlySlice<char> _text;
+        private readonly IReadOnlyList<char> _text;
         private int _position;
         private int _lastPosition;
         private LineBreakClass _currentClass;
@@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
         private int _lb30a;
         private bool _lb31;
 
-        public LineBreakEnumerator(ReadOnlySlice<char> text)
+        public LineBreakEnumerator(IReadOnlyList<char> text)
             : this()
         {
             _text = text;
@@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
                 _lb30a = 0;
             }
 
-            while (_position < _text.Length)
+            while (_position < _text.Count)
             {
                 _lastPosition = _position;
                 var lastClass = _nextClass;
@@ -92,11 +93,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
                 }
             }
 
-            if (_position >= _text.Length)
+            if (_position >= _text.Count)
             {
-                if (_lastPosition < _text.Length)
+                if (_lastPosition < _text.Count)
                 {
-                    _lastPosition = _text.Length;
+                    _lastPosition = _text.Count;
 
                     var required = false;
 

+ 1 - 1
src/Avalonia.Base/Media/TextHitTestResult.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Media
     /// <summary>
     /// Holds a hit test result from a <see cref="TextLayout"/>.
     /// </summary>
-    public readonly struct TextHitTestResult
+    public readonly record struct TextHitTestResult
     {
         public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing)
         {

+ 3 - 8
src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs

@@ -1,21 +1,16 @@
 using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
     public sealed class TextLeadingPrefixTrimming : TextTrimming
     {
-        private readonly ReadOnlySlice<char> _ellipsis;
+        private readonly string _ellipsis;
         private readonly int _prefixLength;
 
-        public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength)
-        {
-        }
-
-        public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
+        public TextLeadingPrefixTrimming(string ellipsis, int prefixLength)
         {
             _prefixLength = prefixLength;
-            _ellipsis = new ReadOnlySlice<char>(ellipsis);
+            _ellipsis = ellipsis;
         }
 
         public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

+ 3 - 8
src/Avalonia.Base/Media/TextTrailingTrimming.cs

@@ -1,21 +1,16 @@
 using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
     public sealed class TextTrailingTrimming : TextTrimming
     {
-        private readonly ReadOnlySlice<char> _ellipsis;
+        private readonly string _ellipsis;
         private readonly bool _isWordBased;
-
-        public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
-        {
-        }
         
-        public TextTrailingTrimming(char[] ellipsis, bool isWordBased)
+        public TextTrailingTrimming(string ellipsis, bool isWordBased)
         {
             _isWordBased = isWordBased;
-            _ellipsis = new ReadOnlySlice<char>(ellipsis);
+            _ellipsis = ellipsis;
         }
 
         public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

+ 1 - 1
src/Avalonia.Base/Media/TextTrimming.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Media
     /// </summary>
     public abstract class TextTrimming
     {
-        internal const char DefaultEllipsisChar = '\u2026';
+        internal const string DefaultEllipsisChar = "\u2026";
 
         /// <summary>
         /// Text is not trimmed.

+ 6 - 6
src/Avalonia.Base/Media/Transformation/TransformOperation.cs

@@ -5,7 +5,7 @@ namespace Avalonia.Media.Transformation
     /// <summary>
     /// Represents a single primitive transform (like translation, rotation, scale, etc.).
     /// </summary>
-    public struct TransformOperation
+    public record struct TransformOperation
     {
         public OperationType Type;
         public Matrix Matrix;
@@ -196,7 +196,7 @@ namespace Avalonia.Media.Transformation
         }
 
         [StructLayout(LayoutKind.Explicit)]
-        public struct DataLayout
+        public record struct DataLayout
         {
             [FieldOffset(0)] public SkewLayout Skew;
 
@@ -206,25 +206,25 @@ namespace Avalonia.Media.Transformation
 
             [FieldOffset(0)] public RotateLayout Rotate;
 
-            public struct SkewLayout
+            public record struct SkewLayout
             {
                 public double X;
                 public double Y;
             }
 
-            public struct ScaleLayout
+            public record struct ScaleLayout
             {
                 public double X;
                 public double Y;
             }
 
-            public struct TranslateLayout
+            public record struct TranslateLayout
             {
                 public double X;
                 public double Y;
             }
 
-            public struct RotateLayout
+            public record struct RotateLayout
             {
                 public double Angle;
             }

+ 1 - 1
src/Avalonia.Base/Media/Transformation/TransformOperations.cs

@@ -165,7 +165,7 @@ namespace Avalonia.Media.Transformation
             return Math.Max(from._operations.Count, to._operations.Count);
         }
 
-        public readonly struct Builder
+        public readonly record struct Builder
         {
             private readonly List<TransformOperation> _operations;
 

+ 2 - 2
src/Avalonia.Base/Media/UnicodeRange.cs

@@ -7,7 +7,7 @@ namespace Avalonia.Media
     /// <summary>
     /// The <see cref="UnicodeRange"/> descripes a set of Unicode characters.
     /// </summary>
-    public readonly struct UnicodeRange
+    public readonly record struct UnicodeRange
     {
         public readonly static UnicodeRange Default = Parse("0-10FFFD");
 
@@ -102,7 +102,7 @@ namespace Avalonia.Media
         }
     }
 
-    public readonly struct UnicodeRangeSegment
+    public readonly record struct UnicodeRangeSegment
     {
         private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$");
 

+ 18 - 0
src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs

@@ -0,0 +1,18 @@
+using System;
+
+namespace Avalonia.Platform;
+
+public interface IOptionalFeatureProvider
+{
+    /// <summary>
+    /// Queries for an optional feature
+    /// </summary>
+    /// <param name="featureType">Feature type</param>
+    public object? TryGetFeature(Type featureType);
+}
+
+public static class OptionalFeatureProviderExtensions
+{
+    public static T? TryGetFeature<T>(this IOptionalFeatureProvider provider) where T : class =>
+        (T?)provider.TryGetFeature(typeof(T));
+}

+ 11 - 3
src/Avalonia.Base/Platform/IPlatformGpu.cs

@@ -4,13 +4,21 @@ using Avalonia.Metadata;
 namespace Avalonia.Platform;
 
 [Unstable]
-public interface IPlatformGpu
+public interface IPlatformGraphics
 {
-    IPlatformGpuContext PrimaryContext { get; }
+    bool UsesSharedContext { get; }
+    IPlatformGraphicsContext CreateContext();
+    IPlatformGraphicsContext GetSharedContext();
 }
 
 [Unstable]
-public interface IPlatformGpuContext : IDisposable
+public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider
 {
+    bool IsLost { get; }
     IDisposable EnsureCurrent();
+}
+
+public class PlatformGraphicsContextLostException : Exception
+{
+    
 }

+ 20 - 9
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -65,15 +65,6 @@ namespace Avalonia.Platform
         /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
         IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun);
 
-        /// <summary>
-        /// Creates a renderer.
-        /// </summary>
-        /// <param name="surfaces">
-        /// The list of native platform surfaces that can be used for output.
-        /// </param>
-        /// <returns>An <see cref="IRenderTarget"/>.</returns>
-        IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces);
-
         /// <summary>
         /// Creates a render target bitmap implementation.
         /// </summary>
@@ -181,6 +172,13 @@ namespace Avalonia.Platform
         /// <returns></returns>
         IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
 
+        /// <summary>
+        /// Creates a backend-specific object using a low-level API graphics context
+        /// </summary>
+        /// <param name="graphicsApiContext">An underlying low-level graphics context (e. g. wrapped OpenGL context, Vulkan device, D3DDevice, etc)</param>
+        /// <returns></returns>
+        IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsApiContext);
+        
         /// <summary>
         /// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
         /// </summary>
@@ -200,4 +198,17 @@ namespace Avalonia.Platform
         /// </summary>
         public PixelFormat DefaultPixelFormat { get; }
     }
+
+    [Unstable]
+    public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable
+    {
+        /// <summary>
+        /// Creates a renderer.
+        /// </summary>
+        /// <param name="surfaces">
+        /// The list of native platform surfaces that can be used for output.
+        /// </param>
+        /// <returns>An <see cref="IRenderTarget"/>.</returns>
+        IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces);
+    }
 }

+ 5 - 5
src/Avalonia.Base/Platform/IRenderTarget.cs

@@ -19,10 +19,10 @@ namespace Avalonia.Platform
         /// to be drawn.
         /// </param>
         IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer);
-    }
-
-    public interface IRenderTargetWithCorruptionInfo : IRenderTarget
-    {
-        bool IsCorrupted { get; }
+        
+        /// <summary>
+        /// Indicates if the render target is no longer usable and needs to be recreated
+        /// </summary>
+        public bool IsCorrupted { get; }
     }
 }

+ 1 - 1
src/Avalonia.Base/Platform/IRuntimePlatform.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Platform
     }
 
     [Unstable]
-    public struct RuntimePlatformInfo
+    public record struct RuntimePlatformInfo
     {
         public OperatingSystemType OperatingSystem { get; set; }
 

+ 2 - 3
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@@ -1,6 +1,5 @@
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
-using Avalonia.Utilities;
 
 namespace Avalonia.Platform
 {
@@ -13,9 +12,9 @@ namespace Avalonia.Platform
         /// <summary>
         /// Shapes the specified region within the text and returns a shaped buffer.
         /// </summary>
-        /// <param name="text">The text.</param>
+        /// <param name="text">The text buffer.</param>
         /// <param name="options">Text shaper options to customize the shaping process.</param>
         /// <returns>A shaped glyph run.</returns>
-        ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options);
+        ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options);
     }   
 }

+ 4 - 0
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@@ -175,6 +175,10 @@ namespace Avalonia.Rendering.Composition.Animations
 
         public override void Activate()
         {
+            if (_finished)
+            {
+                return;
+            }
             TargetObject.Compositor.AddToClock(this);
             base.Activate();
         }

+ 5 - 4
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -38,13 +38,12 @@ public class CompositingRenderer : IRendererWithCompositor
     /// </summary>
     public bool RenderOnlyOnRenderThread { get; set; } = true;
 
-    public CompositingRenderer(IRenderRoot root,
-        Compositor compositor)
+    public CompositingRenderer(IRenderRoot root, Compositor compositor, Func<IEnumerable<object>> surfaces)
     {
         _root = root;
         _compositor = compositor;
         _recordingContext = new DrawingContext(_recorder);
-        CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget);
+        CompositionTarget = compositor.CreateCompositionTarget(surfaces);
         CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
         _update = Update;
     }
@@ -301,7 +300,9 @@ public class CompositingRenderer : IRendererWithCompositor
     {
         CompositionTarget.IsEnabled = false;
     }
-    
+
+    public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType);
+
     public void Dispose()
     {
         Stop();

+ 4 - 3
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using Avalonia.Platform;
 using Avalonia.Rendering.Composition.Animations;
 using Avalonia.Rendering.Composition.Server;
@@ -10,11 +11,11 @@ public partial class Compositor
     /// <summary>
     /// Creates a new CompositionTarget
     /// </summary>
-    /// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
+    /// <param name="surfaces">A factory method to create IRenderTarget to be called from the render thread</param>
     /// <returns></returns>
-    public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory)
+    public CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces)
     {
-        return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
+        return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces));
     }
     
     public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));

+ 32 - 1
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@@ -34,6 +34,7 @@ namespace Avalonia.Rendering.Composition
         internal ServerCompositor Server => _server;
         private Task? _pendingBatch;
         private readonly object _pendingBatchLock = new();
+        private List<Action> _pendingServerCompositorJobs = new();
 
         internal IEasing DefaultEasing { get; }
         
@@ -43,7 +44,7 @@ namespace Avalonia.Rendering.Composition
         /// </summary>
         /// <param name="loop"></param>
         /// <param name="gpu"></param>
-        public Compositor(IRenderLoop loop, IPlatformGpu? gpu)
+        public Compositor(IRenderLoop loop, IPlatformGraphics? gpu)
         {
             Loop = loop;
             _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
@@ -101,6 +102,13 @@ namespace Avalonia.Rendering.Composition
 #endif
                 }
                 _objectsForSerialization.Clear();
+                if (_pendingServerCompositorJobs.Count > 0)
+                {
+                    writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker);
+                    foreach (var job in _pendingServerCompositorJobs)
+                        writer.WriteObject(job);
+                    writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker);
+                }
             }
             
             batch.CommitedAt = Server.Clock.Elapsed;
@@ -136,5 +144,28 @@ namespace Avalonia.Rendering.Composition
             _invokeBeforeCommit.Enqueue(action);
             RequestCommitAsync();
         }
+
+        /// <summary>
+        /// Attempts to query for a feature from the platform render interface
+        /// </summary>
+        public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType)
+        {
+            var tcs = new TaskCompletionSource<object?>();
+            _pendingServerCompositorJobs.Add(() =>
+            {
+                try
+                {
+                    using (Server.RenderInterface.EnsureCurrent())
+                    {
+                        tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType));
+                    }
+                }
+                catch (Exception e)
+                {
+                    tcs.TrySetException(e);
+                }
+            });
+            return new ValueTask<object?>(tcs.Task);
+        }
     }
 }

+ 2 - 1
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics;
 using System.Globalization;
+using System.Linq;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Platform;
@@ -31,7 +32,7 @@ internal class FpsCounter
         {
             var s = new string((char)c, 1);
             var glyph = typeface.GetGlyph((uint)(s[0]));
-            _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph });
+            _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph });
         }
     }
 

+ 9 - 7
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server
     internal partial class ServerCompositionTarget : IDisposable
     {
         private readonly ServerCompositor _compositor;
-        private readonly Func<IRenderTarget> _renderTargetFactory;
+        private readonly Func<IEnumerable<object>> _surfaces;
         private static long s_nextId = 1;
         public long Id { get; }
         public ulong Revision { get; private set; }
@@ -39,11 +39,11 @@ namespace Avalonia.Rendering.Composition.Server
         public ReadbackIndices Readback { get; } = new();
         public int RenderedVisuals { get; set; }
 
-        public ServerCompositionTarget(ServerCompositor compositor, Func<IRenderTarget> renderTargetFactory) :
+        public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces) :
             base(compositor)
         {
             _compositor = compositor;
-            _renderTargetFactory = renderTargetFactory;
+            _surfaces = surfaces;
             Id = Interlocked.Increment(ref s_nextId);
         }
 
@@ -79,13 +79,14 @@ namespace Avalonia.Rendering.Composition.Server
             if (Root == null) 
                 return;
 
-            if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
+            if (_renderTarget?.IsCorrupted == true)
             {
                 _renderTarget!.Dispose();
                 _renderTarget = null;
+                _redrawRequested = true;
             }
 
-            _renderTarget ??= _renderTargetFactory();
+            _renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
 
             Compositor.UpdateServerTime();
             
@@ -109,12 +110,13 @@ namespace Avalonia.Rendering.Composition.Server
             using (var targetContext = _renderTarget.CreateDrawingContext(null))
             {
                 var layerSize = Size * Scaling;
-                if (layerSize != _layerSize || _layer == null)
+                if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
                 {
                     _layer?.Dispose();
                     _layer = null;
                     _layer = targetContext.CreateLayer(Size);
                     _layerSize = layerSize;
+                    _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
                 }
 
                 if (!_dirtyRect.IsEmpty)
@@ -197,7 +199,7 @@ namespace Avalonia.Rendering.Composition.Server
             if(_disposed)
                 return;
             _disposed = true;
-            using (_compositor.GpuContext?.EnsureCurrent())
+            using (_compositor.RenderInterface.EnsureCurrent())
             {
                 if (_layer != null)
                 {

+ 48 - 6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
+using Avalonia.Logging;
 using Avalonia.Platform;
 using Avalonia.Rendering.Composition.Animations;
 using Avalonia.Rendering.Composition.Expressions;
@@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server
     internal class ServerCompositor : IRenderLoopTask
     {
         private readonly IRenderLoop _renderLoop;
+
         private readonly Queue<Batch> _batches = new Queue<Batch>(); 
         public long LastBatchId { get; private set; }
         public Stopwatch Clock { get; } = Stopwatch.StartNew();
@@ -29,13 +31,15 @@ namespace Avalonia.Rendering.Composition.Server
         internal BatchStreamObjectPool<object?> BatchObjectPool;
         internal BatchStreamMemoryPool BatchMemoryPool;
         private object _lock = new object();
-        public IPlatformGpuContext? GpuContext { get; }
+        public PlatformRenderInterfaceContextManager RenderInterface { get; }
+        internal static readonly object RenderThreadJobsStartMarker = new();
+        internal static readonly object RenderThreadJobsEndMarker = new();
 
-        public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu,
+        public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
             BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
         {
-            GpuContext = platformGpu?.PrimaryContext;
             _renderLoop = renderLoop;
+            RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics);
             BatchObjectPool = batchObjectPool;
             BatchMemoryPool = batchMemoryPool;
             _renderLoop.Add(this);
@@ -66,7 +70,14 @@ namespace Avalonia.Rendering.Composition.Server
                 {
                     while (!stream.IsObjectEof)
                     {
-                        var target = (ServerObject)stream.ReadObject()!;
+                        var readObject = stream.ReadObject();
+                        if (readObject == RenderThreadJobsStartMarker)
+                        {
+                            ReadAndExecuteJobs(stream);
+                            continue;
+                        }
+                        
+                        var target = (ServerObject)readObject!;
                         target.DeserializeChanges(stream, batch);
 #if DEBUG_COMPOSITOR_SERIALIZATION
                         if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker)
@@ -84,6 +95,23 @@ namespace Avalonia.Rendering.Composition.Server
             }
         }
 
+        void ReadAndExecuteJobs(BatchStreamReader reader)
+        {
+            object? readObject;
+            while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker)
+            {
+                var job = (Action)readObject!;
+                try
+                {
+                    job();
+                }
+                catch
+                {
+                    // Ignore
+                }
+            }
+        }
+
         void CompletePendingBatches()
         {
             foreach(var batch in _reusableToCompleteList)
@@ -118,8 +146,16 @@ namespace Avalonia.Rendering.Composition.Server
             
             _animationsToUpdate.Clear();
             
-            foreach (var t in _activeTargets)
-                t.Render();
+            try
+            {
+                RenderInterface.EnsureValidBackendContext();
+                foreach (var t in _activeTargets)
+                    t.Render();
+            }
+            catch (Exception e)
+            {
+                Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e);
+            }
         }
 
         public void AddCompositionTarget(ServerCompositionTarget target)
@@ -137,5 +173,11 @@ namespace Avalonia.Rendering.Composition.Server
 
         public void RemoveFromClock(IAnimationInstance animationInstance) =>
             _activeAnimations.Remove(animationInstance);
+
+        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
+        {
+            using (RenderInterface.EnsureCurrent())
+                return RenderInterface.CreateRenderTarget(surfaces);
+        }
     }
 }

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@@ -21,7 +21,7 @@ internal class BatchStreamData
     public Queue<BatchStreamSegment<IntPtr>> Structs { get; } = new();
 }
 
-public struct BatchStreamSegment<TData>
+public record struct BatchStreamSegment<TData>
 {
     public TData Data { get; set; }
     public int ElementCount { get; set; }

+ 49 - 4
src/Avalonia.Base/Rendering/DeferredRenderer.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
+using System.Threading.Tasks;
 using Avalonia.Logging;
 using Avalonia.Media;
 using Avalonia.Media.Immutable;
@@ -22,6 +23,8 @@ namespace Avalonia.Rendering
     {
         private readonly IDispatcher? _dispatcher;
         private readonly IRenderLoop? _renderLoop;
+        private readonly Func<IRenderTarget>? _renderTargetFactory;
+        private readonly PlatformRenderInterfaceContextManager? _renderInterface;
         private readonly Visual _root;
         private readonly ISceneBuilder _sceneBuilder;
 
@@ -39,6 +42,7 @@ namespace Avalonia.Rendering
         private readonly object _startStopLock = new object();
         private readonly object _renderLoopIsRenderingLock = new object();
         private readonly Action _updateSceneIfNeededDelegate;
+        private List<Action>? _pendingRenderThreadJobs;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
@@ -51,6 +55,8 @@ namespace Avalonia.Rendering
         public DeferredRenderer(
             IRenderRoot root,
             IRenderLoop renderLoop,
+            Func<IRenderTarget> renderTargetFactory,
+            PlatformRenderInterfaceContextManager? renderInterface = null,
             ISceneBuilder? sceneBuilder = null,
             IDispatcher? dispatcher = null,
             IDeferredRendererLock? rendererLock = null) : base(true)
@@ -60,6 +66,8 @@ namespace Avalonia.Rendering
             _sceneBuilder = sceneBuilder ?? new SceneBuilder();
             Layers = new RenderLayers();
             _renderLoop = renderLoop;
+            _renderTargetFactory = renderTargetFactory;
+            _renderInterface = renderInterface;
             _lock = rendererLock ?? new ManagedDeferredRendererLock();
             _updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
         }
@@ -256,6 +264,30 @@ namespace Avalonia.Rendering
             }
         }
 
+        public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType)
+        {
+            if (_renderInterface == null)
+                return new((object?)null);
+            
+            var tcs = new TaskCompletionSource<object?>();
+            _pendingRenderThreadJobs ??= new();
+            _pendingRenderThreadJobs.Add(() =>
+            {
+                try
+                {
+                    using (_renderInterface.EnsureCurrent())
+                    {
+                        tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType));
+                    }
+                }
+                catch (Exception e)
+                {
+                    tcs.TrySetException(e);
+                }
+            });
+            return new ValueTask<object?>(tcs.Task);
+        }
+
         bool NeedsUpdate => _dirty == null || _dirty.Count > 0;
         bool IRenderLoopTask.NeedsUpdate => NeedsUpdate;
 
@@ -337,7 +369,16 @@ namespace Avalonia.Rendering
                                 }
                                 finally
                                 {
-                                    scene.Item.MarkAsRendered();
+                                    try
+                                    {
+                                        if(scene.Item.RenderThreadJobs!=null)
+                                            foreach (var job in scene.Item.RenderThreadJobs)
+                                                job();
+                                    }
+                                    finally
+                                    {
+                                        scene.Item.MarkAsRendered();
+                                    }
                                 }
                             }
                         }
@@ -604,7 +645,7 @@ namespace Avalonia.Rendering
                 return;
             }
 
-            if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
+            if (RenderTarget?.IsCorrupted == true)
             {
                 RenderTarget!.Dispose();
                 RenderTarget = null;
@@ -612,7 +653,7 @@ namespace Avalonia.Rendering
 
             if (RenderTarget == null)
             {
-                RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
+                RenderTarget = _renderTargetFactory!();
             }
 
             context = RenderTarget.CreateDrawingContext(this);
@@ -637,7 +678,11 @@ namespace Avalonia.Rendering
             }
             if (_root.IsVisible)
             {
-                var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
+                var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)
+                {
+                    RenderThreadJobs = _pendingRenderThreadJobs
+                });
+                _pendingRenderThreadJobs = null;
                 var scene = sceneRef.Item;
 
                 if (_dirty == null)

+ 0 - 6
src/Avalonia.Base/Rendering/IRenderRoot.cs

@@ -25,12 +25,6 @@ namespace Avalonia.Rendering
         /// </summary>
         double RenderScaling { get; }
 
-        /// <summary>
-        /// Creates a render target for the window.
-        /// </summary>
-        /// <returns>An <see cref="IRenderTarget"/>.</returns>
-        IRenderTarget CreateRenderTarget();
-
         /// <summary>
         /// Adds a rectangle to the window's dirty region.
         /// </summary>

+ 6 - 0
src/Avalonia.Base/Rendering/IRenderer.cs

@@ -1,6 +1,7 @@
 using System;
 using Avalonia.VisualTree;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Avalonia.Rendering.Composition;
 
 namespace Avalonia.Rendering
@@ -87,6 +88,11 @@ namespace Avalonia.Rendering
         /// Stops the renderer.
         /// </summary>
         void Stop();
+
+        /// <summary>
+        /// Attempts to query for a feature from the platform render interface
+        /// </summary>
+        public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType);
     }
     
     public interface IRendererWithCompositor : IRenderer

+ 20 - 6
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading.Tasks;
 using Avalonia.Logging;
 using Avalonia.Media;
 using Avalonia.Platform;
@@ -19,6 +20,8 @@ namespace Avalonia.Rendering
     public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
     {
         private readonly Visual _root;
+        private readonly Func<IRenderTarget> _renderTargetFactory;
+        private readonly PlatformRenderInterfaceContextManager? _renderContext;
         private readonly IRenderRoot? _renderRoot;
         private bool _updateTransformedBounds = true;
         private IRenderTarget? _renderTarget;
@@ -27,15 +30,19 @@ namespace Avalonia.Rendering
         /// Initializes a new instance of the <see cref="ImmediateRenderer"/> class.
         /// </summary>
         /// <param name="root">The control to render.</param>
-        public ImmediateRenderer(Visual root)
+        public ImmediateRenderer(Visual root, Func<IRenderTarget> renderTargetFactory, 
+            PlatformRenderInterfaceContextManager? renderContext = null)
         {
             _root = root ?? throw new ArgumentNullException(nameof(root));
+            _renderTargetFactory = renderTargetFactory;
+            _renderContext = renderContext;
             _renderRoot = root as IRenderRoot;
         }
 
-        private ImmediateRenderer(Visual root, bool updateTransformedBounds)
+        private ImmediateRenderer(Visual root, Func<IRenderTarget> renderTargetFactory, bool updateTransformedBounds)
         {
             _root = root ?? throw new ArgumentNullException(nameof(root));
+            _renderTargetFactory = renderTargetFactory;
             _renderRoot = root as IRenderRoot;
             _updateTransformedBounds = updateTransformedBounds;
         }
@@ -54,7 +61,7 @@ namespace Avalonia.Rendering
         {
             if (_renderTarget == null)
             {
-                _renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
+                _renderTarget = _renderTargetFactory();
             }
 
             try
@@ -104,7 +111,7 @@ namespace Avalonia.Rendering
         /// <param name="target">The render target.</param>
         public static void Render(Visual visual, IRenderTarget target)
         {
-            using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false))
+            using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false))
             using (var context = new DrawingContext(target.CreateDrawingContext(renderer)))
             {
                 renderer.Render(context, visual, visual.Bounds);
@@ -118,7 +125,9 @@ namespace Avalonia.Rendering
         /// <param name="context">The drawing context.</param>
         public static void Render(Visual visual, DrawingContext context)
         {
-            using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false))
+            using (var renderer = new ImmediateRenderer(visual, 
+                       () => throw new InvalidOperationException("This is not supposed to be called"),
+                       updateTransformedBounds: false))
             {
                 renderer.Render(context, visual, visual.Bounds);
             }
@@ -185,6 +194,9 @@ namespace Avalonia.Rendering
         {
         }
 
+        public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) =>
+            new(_renderContext?.Value?.TryGetFeature(featureType));
+
         /// <inheritdoc/>
         Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
         {
@@ -201,7 +213,9 @@ namespace Avalonia.Rendering
 
         internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
         {
-            using var renderer = new ImmediateRenderer(visual, updateTransformedBounds);
+            using var renderer = new ImmediateRenderer(visual, 
+                () => throw new InvalidOperationException("This is not supposed to be called"),
+                updateTransformedBounds);
             renderer.Render(context, visual, visual.Bounds);
         }
 

+ 24 - 0
src/Avalonia.Base/Rendering/OwnedDisposable.cs

@@ -0,0 +1,24 @@
+using System;
+
+namespace Avalonia.Rendering;
+
+struct OwnedDisposable<T> :IDisposable where T : class, IDisposable
+{
+    private readonly bool _owns;
+    private T? _value;
+
+    public T Value => _value ?? throw new ObjectDisposedException("OwnedDisposable");
+
+    public OwnedDisposable(T value, bool owns)
+    {
+        _owns = owns;
+        _value = value;
+    }
+
+    public void Dispose()
+    {
+        if(_owns)
+            _value?.Dispose();
+        _value = null;
+    }
+}

+ 66 - 0
src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering;
+
+[Unstable]
+// TODO: Make it internal once legacy renderers are removed
+public class PlatformRenderInterfaceContextManager
+{
+    private readonly IPlatformGraphics? _graphics;
+    private IPlatformRenderInterfaceContext? _backend;
+    private OwnedDisposable<IPlatformGraphicsContext>? _gpuContext;
+
+    public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics)
+    {
+        _graphics = graphics;
+    }
+
+    public void EnsureValidBackendContext()
+    {
+        if (_backend == null || _gpuContext?.Value.IsLost == true)
+        {
+            _backend?.Dispose();
+            _backend = null;
+            _gpuContext?.Dispose();
+            _gpuContext = null;
+
+            if (_graphics != null)
+            {
+                if (_graphics.UsesSharedContext)
+                    _gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.GetSharedContext(), false);
+                else
+                    _gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.CreateContext(), true);
+            }
+
+            _backend = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()
+                .CreateBackendContext(_gpuContext?.Value);
+        }
+    }
+
+    public IPlatformRenderInterfaceContext Value
+    {
+        get
+        {
+            EnsureValidBackendContext();
+            return _backend!;
+        }
+    }
+
+    public IDisposable EnsureCurrent()
+    {
+        EnsureValidBackendContext();
+        if (_gpuContext.HasValue)
+            return _gpuContext.Value.Value.EnsureCurrent();
+        return Disposable.Empty;
+    }
+    
+    public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
+    {
+        EnsureValidBackendContext();
+        return _backend!.CreateRenderTarget(surfaces);
+    }
+}

+ 15 - 11
src/Avalonia.Base/Rendering/RenderLoop.cs

@@ -19,6 +19,7 @@ namespace Avalonia.Rendering
         private readonly IDispatcher _dispatcher;
         private List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
         private List<IRenderLoopTask> _itemsCopy = new List<IRenderLoopTask>();
+        private List<IRenderLoopTask> _updateItemsCopy = new List<IRenderLoopTask>();
         private IRenderTimer? _timer;
         private int _inTick;
         private int _inUpdate;
@@ -97,7 +98,13 @@ namespace Avalonia.Rendering
                 {
                     bool needsUpdate = false;
 
-                    foreach (IRenderLoopTask item in _items)
+                    lock (_items)
+                    {
+                        _itemsCopy.Clear();
+                        _itemsCopy.AddRange(_items);
+                    }
+                    
+                    foreach (IRenderLoopTask item in _itemsCopy)
                     {
                         if (item.NeedsUpdate)
                         {
@@ -112,10 +119,13 @@ namespace Avalonia.Rendering
                     {
                         _dispatcher.Post(() =>
                         {
-                            for (var i = 0; i < _items.Count; ++i)
+                            lock (_items)
+                            {
+                                _updateItemsCopy.Clear();
+                                _updateItemsCopy.AddRange(_items);
+                            }
+                            foreach (var item in _updateItemsCopy)
                             {
-                                var item = _items[i];
-
                                 if (item.NeedsUpdate)
                                 {
                                     try
@@ -128,18 +138,12 @@ namespace Avalonia.Rendering
                                     }
                                 }
                             }
+                            _updateItemsCopy.Clear();
 
                             Interlocked.Exchange(ref _inUpdate, 0);
                         }, DispatcherPriority.Render);
                     }
 
-                    lock (_items)
-                    {
-                        _itemsCopy.Clear();
-                        foreach (var i in _items)
-                            _itemsCopy.Add(i);
-                    }
-
                     for (int i = 0; i < _itemsCopy.Count; i++)
                     {
                         _itemsCopy[i].Render();

+ 2 - 0
src/Avalonia.Base/Rendering/SceneGraph/Scene.cs

@@ -346,5 +346,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         public void MarkAsRendered() => _rendered.TrySetResult(true);
+
+        public List<Action>? RenderThreadJobs { get; set; }
     }
 }

+ 1 - 1
src/Avalonia.Base/Styling/SelectorMatch.cs

@@ -43,7 +43,7 @@ namespace Avalonia.Styling
     /// A selector match describes whether and how a <see cref="Selector"/> matches a control, and
     /// in addition whether the selector can ever match a control of the same type.
     /// </remarks>
-    public readonly struct SelectorMatch
+    public readonly record struct SelectorMatch
     {
         /// <summary>
         /// A selector match with the result of <see cref="SelectorMatchResult.NeverThisType"/>.

+ 0 - 8
src/Avalonia.Base/Utilities/ArraySlice.cs

@@ -111,14 +111,6 @@ namespace Avalonia.Utilities
             }
         }
 
-        /// <summary>
-        /// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
-        /// </summary>
-        public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice)
-        {
-            return new ReadOnlySlice<T>(slice._data, 0, slice.Length, slice.Start);
-        }
-
         /// <summary>
         /// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
         /// </summary>

+ 0 - 239
src/Avalonia.Base/Utilities/ReadOnlySlice.cs

@@ -1,239 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
-
-namespace Avalonia.Utilities
-{
-    /// <summary>
-    ///     ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
-    /// </summary>
-    /// <typeparam name="T">The type of elements in the slice.</typeparam>
-    [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
-    public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
-    {
-        private readonly int _bufferOffset;
-        
-        /// <summary>
-        /// Gets an empty <see cref="ReadOnlySlice{T}"/>
-        /// </summary>
-        public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>());
-        
-        private readonly ReadOnlyMemory<T> _buffer;
-
-        public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
-
-        public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int bufferOffset = 0)
-        {
-#if DEBUG
-            if (start.CompareTo(0) < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof (start));
-            }
-
-            if (length.CompareTo(buffer.Length) > 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof (length));
-            }
-#endif
-            
-            _buffer = buffer;
-            Start = start;
-            Length = length;
-            _bufferOffset = bufferOffset;
-        }
-
-        /// <summary>
-        ///     Gets the start.
-        /// </summary>
-        /// <value>
-        ///     The start.
-        /// </value>
-        public int Start { get; }
-
-        /// <summary>
-        ///     Gets the end.
-        /// </summary>
-        /// <value>
-        ///     The end.
-        /// </value>
-        public int End => Start + Length - 1;
-
-        /// <summary>
-        ///     Gets the length.
-        /// </summary>
-        /// <value>
-        ///     The length.
-        /// </value>
-        public int Length { get; }
-
-        /// <summary>
-        ///     Gets a value that indicates whether this instance of <see cref="ReadOnlySlice{T}"/> is Empty.
-        /// </summary>
-        public bool IsEmpty => Length == 0;
-
-        /// <summary>
-        ///     Get the underlying span.
-        /// </summary>
-        public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length);
-
-        /// <summary>
-        ///     Get the buffer offset.
-        /// </summary>
-        public int BufferOffset => _bufferOffset;
-        
-        /// <summary>
-        ///     Get the underlying buffer.
-        /// </summary>
-        public ReadOnlyMemory<T> Buffer => _buffer;
-
-        /// <summary>
-        /// Returns a value to specified element of the slice.
-        /// </summary>
-        /// <param name="index">The index of the element to return.</param>
-        /// <returns>The <typeparamref name="T"/>.</returns>
-        /// <exception cref="IndexOutOfRangeException">
-        /// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
-        /// </exception>
-        public T this[int index]
-        {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            get
-            {
-#if DEBUG
-                if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
-                {
-                    throw new ArgumentOutOfRangeException(nameof (index));
-                }
-#endif
-                return Span[index];
-            }
-        }
-        
-        /// <summary>
-        ///     Returns a sub slice of elements that start at the specified index and has the specified number of elements.
-        /// </summary>
-        /// <param name="start">The start of the sub slice.</param>
-        /// <param name="length">The length of the sub slice.</param>
-        /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
-        public ReadOnlySlice<T> AsSlice(int start, int length)
-        {
-            if (IsEmpty)
-            {
-                return this;
-            }
-
-            if (length == 0)
-            {
-                return Empty;
-            }
-
-            if (start < 0 || _bufferOffset + start > _buffer.Length - 1)
-            {
-                throw new ArgumentOutOfRangeException(nameof(start));
-            }
-
-            if (_bufferOffset + start + length > _buffer.Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(length));
-            }
-
-            return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset);
-        }
-
-        /// <summary>
-        ///     Returns a specified number of contiguous elements from the start of the slice.
-        /// </summary>
-        /// <param name="length">The number of elements to return.</param>
-        /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
-        public ReadOnlySlice<T> Take(int length)
-        {
-            if (IsEmpty)
-            {
-                return this;
-            }
-
-            if (length > Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(length));
-            }
-
-            return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset);
-        }
-
-        /// <summary>
-        ///     Bypasses a specified number of elements in the slice and then returns the remaining elements.
-        /// </summary>
-        /// <param name="length">The number of elements to skip before returning the remaining elements.</param>
-        /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
-        public ReadOnlySlice<T> Skip(int length)
-        {
-            if (IsEmpty)
-            {
-                return this;
-            }
-
-            if (length > Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(length));
-            }
-
-            return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length);
-        }
-
-        /// <summary>
-        /// Returns an enumerator for the slice.
-        /// </summary>
-        public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
-        {
-            return new ImmutableReadOnlyListStructEnumerator<T>(this);
-        }
-
-        IEnumerator<T> IEnumerable<T>.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        int IReadOnlyCollection<T>.Count => Length;
-
-        T IReadOnlyList<T>.this[int index] => this[index];
-
-        public static implicit operator ReadOnlySlice<T>(T[] array)
-        {
-            return new ReadOnlySlice<T>(array);
-        }
-
-        public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
-        {
-            return new ReadOnlySlice<T>(memory);
-        }
-
-        public static implicit operator ReadOnlySpan<T>(ReadOnlySlice<T> slice) => slice.Span;
-
-        internal class ReadOnlySliceDebugView
-        {
-            private readonly ReadOnlySlice<T> _readOnlySlice;
-
-            public ReadOnlySliceDebugView(ReadOnlySlice<T> readOnlySlice)
-            {
-                _readOnlySlice = readOnlySlice;
-            }
-
-            public int Start => _readOnlySlice.Start;
-
-            public int End => _readOnlySlice.End;
-
-            public int Length => _readOnlySlice.Length;
-
-            public bool IsEmpty => _readOnlySlice.IsEmpty;
-
-            public ReadOnlySpan<T> Items => _readOnlySlice.Span;
-        }
-    }
-}

+ 1 - 1
src/Avalonia.Base/Utilities/SmallDictionary.cs

@@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
 
 namespace Avalonia.Utilities;
 
-public struct InlineDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>> where TKey : class where TValue : class
+public record struct InlineDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>> where TKey : class where TValue : class
 {
     object? _data;
     TValue? _value;

+ 1 - 1
src/Avalonia.Base/Utilities/StringTokenizer.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Utilities
 #if !BUILDTASK
     public
 #endif
-    struct StringTokenizer : IDisposable
+    record struct StringTokenizer : IDisposable
     {
         private const char DefaultSeparatorChar = ',';
 

+ 1 - 1
src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Utilities
     /// A task-like operation that is guaranteed to finish continuations synchronously,
     /// can be used for parametrized one-shot events
     /// </summary>
-    public struct SynchronousCompletionAsyncResult<T> : INotifyCompletion
+    public record struct SynchronousCompletionAsyncResult<T> : INotifyCompletion
     {
         private readonly SynchronousCompletionAsyncResultSource<T>? _source;
         private readonly T? _result;

+ 1 - 1
src/Avalonia.Base/Utilities/ValueSpan.cs

@@ -3,7 +3,7 @@
     /// <summary>
     /// Pairing of value and positions sharing that value.
     /// </summary>
-    public readonly struct ValueSpan<T>
+    public readonly record struct ValueSpan<T>
     {
         public ValueSpan(int start, int length, T value)
         {

+ 0 - 4
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@@ -3,10 +3,6 @@
                     xmlns:controls="using:Avalonia.Controls"
                     xmlns:primitives="using:Avalonia.Controls.Primitives">
 
-  <ResourceDictionary.MergedDictionaries>
-    <ResourceInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml" />
-  </ResourceDictionary.MergedDictionaries>
-
   <ControlTheme x:Key="{x:Type ColorPicker}"
                 TargetType="ColorPicker">
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно