Răsfoiți Sursa

Merge branch 'master' into refactor/itemcontainergenerator

Steven Kirk 2 ani în urmă
părinte
comite
54924fca0e
100 a modificat fișierele cu 2258 adăugiri și 723 ștergeri
  1. 37 9
      NOTICE.md
  2. 1 0
      build/Base.props
  3. 0 1
      samples/ControlCatalog/Pages/MenuPage.xaml.cs
  4. 0 1
      samples/ControlCatalog/Pages/PointerContactsTab.cs
  5. 0 1
      samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs
  6. 0 1
      samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
  7. 0 1
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  8. 1 1
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  9. 0 2
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  10. 1 2
      samples/ControlCatalog/ViewModels/NotificationViewModel.cs
  11. 0 1
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  12. 0 1
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  13. 3 1
      samples/MiniMvvm/MiniMvvm.csproj
  14. 8 6
      samples/MiniMvvm/PropertyChangedExtensions.cs
  15. 0 1
      samples/MiniMvvm/ViewModelBase.cs
  16. 1 0
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  17. 2 1
      samples/Previewer/Previewer.csproj
  18. 0 1
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  19. 0 2
      samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
  20. 1 1
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  21. 1 1
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  22. 1 2
      src/Avalonia.Base/Animation/Animation.cs
  23. 1 3
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  24. 1 1
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  25. 0 2
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  26. 1 1
      src/Avalonia.Base/Animation/Animators/ColorAnimator.cs
  27. 1 1
      src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
  28. 1 0
      src/Avalonia.Base/Animation/Clock.cs
  29. 2 2
      src/Avalonia.Base/Animation/CrossFade.cs
  30. 11 1
      src/Avalonia.Base/Avalonia.Base.csproj
  31. 55 121
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  32. 3 3
      src/Avalonia.Base/AvaloniaProperty`1.cs
  33. 1 0
      src/Avalonia.Base/ClassBindingManager.cs
  34. 1 1
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  35. 0 1
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  36. 1 1
      src/Avalonia.Base/Controls/NameScopeLocator.cs
  37. 5 6
      src/Avalonia.Base/Data/BindingOperations.cs
  38. 0 1
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  39. 2 4
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  40. 8 9
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  41. 38 23
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  42. 24 41
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  43. 2 3
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  44. 1 1
      src/Avalonia.Base/Data/Core/StreamNode.cs
  45. 7 2
      src/Avalonia.Base/Data/IndexerBinding.cs
  46. 2 3
      src/Avalonia.Base/Data/IndexerDescriptor.cs
  47. 20 30
      src/Avalonia.Base/Data/InstancedBinding.cs
  48. 1 0
      src/Avalonia.Base/Input/Gestures.cs
  49. 1 0
      src/Avalonia.Base/Input/InputElement.cs
  50. 4 4
      src/Avalonia.Base/Input/InputManager.cs
  51. 1 0
      src/Avalonia.Base/Input/MouseDevice.cs
  52. 1 1
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  53. 1 2
      src/Avalonia.Base/Interactivity/InteractiveExtensions.cs
  54. 3 3
      src/Avalonia.Base/Interactivity/RoutedEvent.cs
  55. 7 11
      src/Avalonia.Base/Layout/Layoutable.cs
  56. 4 5
      src/Avalonia.Base/Media/Brush.cs
  57. 5 6
      src/Avalonia.Base/Media/DashStyle.cs
  58. 4 5
      src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs
  59. 3 1
      src/Avalonia.Base/Media/Geometry.cs
  60. 1 0
      src/Avalonia.Base/Media/GradientBrush.cs
  61. 1 0
      src/Avalonia.Base/Media/MatrixTransform.cs
  62. 1 0
      src/Avalonia.Base/Media/RotateTransform.cs
  63. 11 2
      src/Avalonia.Base/Media/ScaleTransform.cs
  64. 11 2
      src/Avalonia.Base/Media/SkewTransform.cs
  65. 4 7
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  66. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  67. 13 10
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  68. 117 101
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  69. 6 1
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  70. 72 61
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  71. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
  72. 76 30
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  73. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  74. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  75. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  76. 11 2
      src/Avalonia.Base/Media/TranslateTransform.cs
  77. 1 1
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  78. 62 0
      src/Avalonia.Base/Reactive/AnonymousObserver.cs
  79. 23 0
      src/Avalonia.Base/Reactive/CombinedSubject.cs
  80. 427 0
      src/Avalonia.Base/Reactive/CompositeDisposable.cs
  81. 98 0
      src/Avalonia.Base/Reactive/Disposable.cs
  82. 37 0
      src/Avalonia.Base/Reactive/DisposableMixin.cs
  83. 8 0
      src/Avalonia.Base/Reactive/IAvaloniaSubject.cs
  84. 4 4
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  85. 30 0
      src/Avalonia.Base/Reactive/LightweightSubject.cs
  86. 247 0
      src/Avalonia.Base/Reactive/Observable.cs
  87. 0 37
      src/Avalonia.Base/Reactive/ObservableEx.cs
  88. 374 0
      src/Avalonia.Base/Reactive/Operators/CombineLatest.cs
  89. 111 0
      src/Avalonia.Base/Reactive/Operators/Sink.cs
  90. 144 0
      src/Avalonia.Base/Reactive/Operators/Switch.cs
  91. 35 0
      src/Avalonia.Base/Reactive/SerialDisposableValue.cs
  92. 1 1
      src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs
  93. 1 1
      src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs
  94. 1 1
      src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs
  95. 3 3
      src/Avalonia.Base/Styling/StyleInstance.cs
  96. 1 1
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  97. 0 18
      src/Avalonia.Base/Utilities/IWeakSubscriber.cs
  98. 0 60
      src/Avalonia.Base/Utilities/WeakObservable.cs
  99. 39 36
      src/Avalonia.Base/Visual.cs
  100. 0 1
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

+ 37 - 9
NOTICE.md

@@ -81,14 +81,14 @@ A "contributor" is any person that distributes its contribution under this licen
 
 https://github.com/wayland-project/wayland-protocols
 
-Copyright © 2008-2013 Kristian Høgsberg
-Copyright © 2010-2013 Intel Corporation
-Copyright © 2013      Rafael Antognolli
-Copyright © 2013      Jasper St. Pierre
-Copyright © 2014      Jonas Ådahl
-Copyright © 2014      Jason Ekstrand
-Copyright © 2014-2015 Collabora, Ltd.
-Copyright © 2015      Red Hat Inc.
+Copyright © 2008-2013 Kristian Høgsberg
+Copyright © 2010-2013 Intel Corporation
+Copyright © 2013      Rafael Antognolli
+Copyright © 2013      Jasper St. Pierre
+Copyright © 2014      Jonas Ådahl
+Copyright © 2014      Jason Ekstrand
+Copyright © 2014-2015 Collabora, Ltd.
+Copyright © 2015      Red Hat Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a
 copy of this software and associated documentation files (the "Software"),
@@ -140,7 +140,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 https://github.com/toptensoftware/RichTextKit
 
-Copyright © 2019 Topten Software. All Rights Reserved.
+Copyright © 2019 Topten Software. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License"); you may 
 not use this product except in compliance with the License. You may obtain 
@@ -334,3 +334,31 @@ https://github.com/flutter/flutter
 //ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 //(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 //SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Reactive Extensions
+
+https://github.com/dotnet/reactive
+
+The MIT License (MIT)
+
+Copyright (c) .NET Foundation and Contributors
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 1 - 0
build/Base.props

@@ -1,6 +1,7 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup Condition="'$(TargetFramework)' != 'net6'">
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
+    <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />
   </ItemGroup>
 </Project>

+ 0 - 1
samples/ControlCatalog/Pages/MenuPage.xaml.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive;
 using System.Threading.Tasks;
 using System.Windows.Input;
 using Avalonia.Controls;

+ 0 - 1
samples/ControlCatalog/Pages/PointerContactsTab.cs

@@ -2,7 +2,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Reactive.Linq;
 
 using Avalonia;
 using Avalonia.Controls;

+ 0 - 1
samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.ObjectModel;
 using System.Linq;
-using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Controls.Selection;
 using MiniMvvm;

+ 0 - 1
samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using System.Reactive;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.VisualTree;

+ 0 - 1
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.ObjectModel;
 using System.Linq;
-using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Controls.Selection;
 using ControlCatalog.Pages;

+ 1 - 1
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -1,9 +1,9 @@
-using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.Notifications;
 using Avalonia.Dialogs;
 using Avalonia.Platform;
+using Avalonia.Reactive;
 using System;
 using System.ComponentModel.DataAnnotations;
 using MiniMvvm;

+ 0 - 2
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@@ -1,6 +1,4 @@
 using System.Collections.Generic;
-using System.Reactive;
-using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.VisualTree;

+ 1 - 2
samples/ControlCatalog/ViewModels/NotificationViewModel.cs

@@ -1,5 +1,4 @@
-using System.Reactive;
-using Avalonia.Controls.Notifications;
+using Avalonia.Controls.Notifications;
 using MiniMvvm;
 
 namespace ControlCatalog.ViewModels

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

@@ -1,6 +1,5 @@
 using System.Collections.ObjectModel;
 using System.Linq;
-using System.Reactive;
 using System.Threading.Tasks;
 using Avalonia.Controls.Notifications;
 using ControlCatalog.Pages;

+ 0 - 1
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.ObjectModel;
 using System.Linq;
-using System.Reactive;
 using Avalonia.Controls;
 using MiniMvvm;
 

+ 3 - 1
samples/MiniMvvm/MiniMvvm.csproj

@@ -2,5 +2,7 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
   </PropertyGroup>
-  <Import Project="..\..\build\Rx.props" />
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
+  </ItemGroup>
 </Project>

+ 8 - 6
samples/MiniMvvm/PropertyChangedExtensions.cs

@@ -1,8 +1,8 @@
 using System;
 using System.ComponentModel;
 using System.Linq.Expressions;
-using System.Reactive.Linq;
 using System.Reflection;
+using Avalonia.Reactive;
 
 namespace MiniMvvm
 {
@@ -92,11 +92,13 @@ namespace MiniMvvm
             Expression<Func<TModel, T3>> v3,
             Func<T1, T2, T3, TRes> cb
         ) where TModel : INotifyPropertyChanged =>
-            Observable.CombineLatest(
-                model.WhenAnyValue(v1),
-                model.WhenAnyValue(v2),
-                model.WhenAnyValue(v3),
-                cb);
+            model.WhenAnyValue(v1)
+                .CombineLatest(
+                    model.WhenAnyValue(v2),
+                    (l, r) => (l, r))
+                .CombineLatest(
+                    model.WhenAnyValue(v3),
+                    (t, r) => cb(t.l, t.r, r));
 
         public static IObservable<ValueTuple<T1, T2, T3>> WhenAnyValue<TModel, T1, T2, T3>(this TModel model,
             Expression<Func<TModel, T1>> v1,

+ 0 - 1
samples/MiniMvvm/ViewModelBase.cs

@@ -1,6 +1,5 @@
 using System.Collections.Generic;
 using System.ComponentModel;
-using System.Reactive.Joins;
 using System.Runtime.CompilerServices;
 
 namespace MiniMvvm

+ 1 - 0
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@@ -11,4 +11,5 @@
     <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
   </ItemGroup>
 
+  <Import Project="..\..\build\Rx.props" />
 </Project>

+ 2 - 1
samples/Previewer/Previewer.csproj

@@ -12,7 +12,8 @@
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
   </ItemGroup>
-  
+
+  <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\SampleApp.props" />
   <Import Project="..\..\build\ReferenceCoreLibraries.props" />
 </Project>

+ 0 - 1
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

@@ -23,6 +23,5 @@
   <Import Project="..\..\build\SampleApp.props" />
   <Import Project="..\..\build\ReferenceCoreLibraries.props" />
   <Import Project="..\..\build\BuildTargets.targets" />
-  <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\ReactiveUI.props" />
 </Project>

+ 0 - 2
samples/interop/WindowsInteropTest/WindowsInteropTest.csproj

@@ -15,6 +15,4 @@
       <Name>ControlCatalog</Name>
     </ProjectReference>
   </ItemGroup>
-
-  <Import Project="..\..\..\build\Rx.props" />
 </Project>

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

@@ -1,10 +1,10 @@
 using System;
-using System.Reactive.Disposables;
 using System.Threading;
 
 using Android.OS;
 
 using Avalonia.Platform;
+using Avalonia.Reactive;
 using Avalonia.Threading;
 
 using App = Android.App.Application;

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

@@ -1,11 +1,11 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive.Disposables;
 using System.Threading.Tasks;
 
 using Android.OS;
 using Android.Views;
 
+using Avalonia.Reactive;
 using Avalonia.Rendering;
 
 using Java.Lang;

+ 1 - 2
src/Avalonia.Base/Animation/Animation.cs

@@ -2,8 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 using System.Threading;
 using System.Threading.Tasks;
 

+ 1 - 3
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@@ -1,10 +1,8 @@
 using System;
 using System.Linq;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 using Avalonia.Animation.Animators;
-using Avalonia.Animation.Utils;
 using Avalonia.Data;
-using Avalonia.Reactive;
 
 namespace Avalonia.Animation
 {

+ 1 - 1
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Animation
             }
             else
             {
-                return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
+                return this.Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
             }
         }
 

+ 0 - 2
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@@ -1,8 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
 using Avalonia.Animation.Utils;
 using Avalonia.Collections;
 using Avalonia.Data;

+ 1 - 1
src/Avalonia.Base/Animation/Animators/ColorAnimator.cs

@@ -2,7 +2,7 @@
 // and adopted from LottieSharp Project (https://github.com/ascora/LottieSharp).
 
 using System;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Logging;
 using Avalonia.Media;
 

+ 1 - 1
src/Avalonia.Base/Animation/Animators/TransformAnimator.cs

@@ -1,5 +1,5 @@
 using System;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Logging;
 using Avalonia.Media;
 using Avalonia.Media.Transformation;

+ 1 - 0
src/Avalonia.Base/Animation/Clock.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 
 namespace Avalonia.Animation
 {

+ 2 - 2
src/Avalonia.Base/Animation/CrossFade.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using System.Threading;
 using System.Threading.Tasks;
 using Avalonia.Animation.Easings;
@@ -108,7 +108,7 @@ namespace Avalonia.Animation
             }
 
             var tasks = new List<Task>();
-            using (var disposables = new CompositeDisposable())
+            using (var disposables = new CompositeDisposable(1))
             {
                 if (to != null)
                 {

+ 11 - 1
src/Avalonia.Base/Avalonia.Base.csproj

@@ -14,7 +14,6 @@
   </ItemGroup>
   <Import Project="..\..\build\Base.props" />
   <Import Project="..\..\build\Binding.props" />
-  <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
@@ -37,6 +36,13 @@
     <InternalsVisibleTo Include="Avalonia.Skia, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.FreeDesktop, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Browser, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Skia, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Direct2D1.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
@@ -48,8 +54,12 @@
     <InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.iOS, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="MiniMvvm, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
   </ItemGroup>
   

+ 55 - 121
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -1,10 +1,6 @@
 using System;
-using System.Reactive;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
-using Avalonia.Data;
 using Avalonia.Reactive;
+using Avalonia.Data;
 
 namespace Avalonia
 {
@@ -127,108 +123,6 @@ namespace Avalonia
                 property ?? throw new ArgumentNullException(nameof(property)));
         }
 
-        /// <summary>
-        /// Gets a subject for an <see cref="AvaloniaProperty"/>.
-        /// </summary>
-        /// <param name="o">The object.</param>
-        /// <param name="property">The property.</param>
-        /// <param name="priority">
-        /// The priority with which binding values are written to the object.
-        /// </param>
-        /// <returns>
-        /// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the 
-        /// property.
-        /// </returns>
-        public static ISubject<object?> GetSubject(
-            this AvaloniaObject o,
-            AvaloniaProperty property,
-            BindingPriority priority = BindingPriority.LocalValue)
-        {
-            return Subject.Create<object?>(
-                Observer.Create<object?>(x => o.SetValue(property, x, priority)),
-                o.GetObservable(property));
-        }
-
-        /// <summary>
-        /// Gets a subject for an <see cref="AvaloniaProperty"/>.
-        /// </summary>
-        /// <typeparam name="T">The property type.</typeparam>
-        /// <param name="o">The object.</param>
-        /// <param name="property">The property.</param>
-        /// <param name="priority">
-        /// The priority with which binding values are written to the object.
-        /// </param>
-        /// <returns>
-        /// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the 
-        /// property.
-        /// </returns>
-        public static ISubject<T> GetSubject<T>(
-            this AvaloniaObject o,
-            AvaloniaProperty<T> property,
-            BindingPriority priority = BindingPriority.LocalValue)
-        {
-            return Subject.Create<T>(
-                Observer.Create<T>(x => o.SetValue(property, x, priority)),
-                o.GetObservable(property));
-        }
-
-        /// <summary>
-        /// Gets a subject for a <see cref="AvaloniaProperty"/>.
-        /// </summary>
-        /// <param name="o">The object.</param>
-        /// <param name="property">The property.</param>
-        /// <param name="priority">
-        /// The priority with which binding values are written to the object.
-        /// </param>
-        /// <returns>
-        /// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the 
-        /// property.
-        /// </returns>
-        public static ISubject<BindingValue<object?>> GetBindingSubject(
-            this AvaloniaObject o,
-            AvaloniaProperty property,
-            BindingPriority priority = BindingPriority.LocalValue)
-        {
-            return Subject.Create<BindingValue<object?>>(
-                Observer.Create<BindingValue<object?>>(x =>
-                {
-                    if (x.HasValue)
-                    {
-                        o.SetValue(property, x.Value, priority);
-                    }
-                }),
-                o.GetBindingObservable(property));
-        }
-
-        /// <summary>
-        /// Gets a subject for a <see cref="AvaloniaProperty"/>.
-        /// </summary>
-        /// <typeparam name="T">The property type.</typeparam>
-        /// <param name="o">The object.</param>
-        /// <param name="property">The property.</param>
-        /// <param name="priority">
-        /// The priority with which binding values are written to the object.
-        /// </param>
-        /// <returns>
-        /// An <see cref="ISubject{T}"/> which can be used for two-way binding to/from the 
-        /// property.
-        /// </returns>
-        public static ISubject<BindingValue<T>> GetBindingSubject<T>(
-            this AvaloniaObject o,
-            AvaloniaProperty<T> property,
-            BindingPriority priority = BindingPriority.LocalValue)
-        {
-            return Subject.Create<BindingValue<T>>(
-                Observer.Create<BindingValue<T>>(x =>
-                {
-                    if (x.HasValue)
-                    {
-                        o.SetValue(property, x.Value, priority);
-                    }
-                }),
-                o.GetBindingObservable(property));
-        }
-
         /// <summary>
         /// Binds an <see cref="AvaloniaProperty"/> to an observable.
         /// </summary>
@@ -407,13 +301,7 @@ namespace Avalonia
             Action<TTarget, AvaloniaPropertyChangedEventArgs> action)
             where TTarget : AvaloniaObject
         {
-            return observable.Subscribe(e =>
-            {
-                if (e.Sender is TTarget target)
-                {
-                    action(target, e);
-                }
-            });
+            return observable.Subscribe(new ClassHandlerObserver<TTarget>(action));
         }
 
         /// <summary>
@@ -431,13 +319,7 @@ namespace Avalonia
             this IObservable<AvaloniaPropertyChangedEventArgs<TValue>> observable,
             Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> action) where TTarget : AvaloniaObject
         {
-            return observable.Subscribe(e =>
-            {
-                if (e.Sender is TTarget target)
-                {
-                    action(target, e);
-                }
-            });
+            return observable.Subscribe(new ClassHandlerObserver<TTarget, TValue>(action));
         }
 
         private class BindingAdaptor : IBinding
@@ -458,5 +340,57 @@ namespace Avalonia
                 return InstancedBinding.OneWay(_source);
             }
         }
+        
+        private class ClassHandlerObserver<TTarget, TValue> : IObserver<AvaloniaPropertyChangedEventArgs<TValue>>
+        {
+            private readonly Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> _action;
+
+            public ClassHandlerObserver(Action<TTarget, AvaloniaPropertyChangedEventArgs<TValue>> action)
+            {
+                _action = action;
+            }
+
+            public void OnCompleted()
+            {
+            }
+
+            public void OnError(Exception error)
+            {
+            }
+
+            public void OnNext(AvaloniaPropertyChangedEventArgs<TValue> value)
+            {
+                if (value.Sender is TTarget target)
+                {
+                    _action(target, value);
+                }
+            }
+        }
+
+        private class ClassHandlerObserver<TTarget> : IObserver<AvaloniaPropertyChangedEventArgs>
+        {
+            private readonly Action<TTarget, AvaloniaPropertyChangedEventArgs> _action;
+
+            public ClassHandlerObserver(Action<TTarget, AvaloniaPropertyChangedEventArgs> action)
+            {
+                _action = action;
+            }
+
+            public void OnCompleted()
+            {
+            }
+
+            public void OnError(Exception error)
+            {
+            }
+
+            public void OnNext(AvaloniaPropertyChangedEventArgs value)
+            {
+                if (value.Sender is TTarget target)
+                {
+                    _action(target, value);
+                }
+            }
+        }
     }
 }

+ 3 - 3
src/Avalonia.Base/AvaloniaProperty`1.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
-using System.Reactive.Subjects;
 using Avalonia.Data;
+using Avalonia.Reactive;
 using Avalonia.Utilities;
 
 namespace Avalonia
@@ -12,7 +12,7 @@ namespace Avalonia
     /// <typeparam name="TValue">The value type of the property.</typeparam>
     public abstract class AvaloniaProperty<TValue> : AvaloniaProperty
     {
-        private readonly Subject<AvaloniaPropertyChangedEventArgs<TValue>> _changed;
+        private readonly LightweightSubject<AvaloniaPropertyChangedEventArgs<TValue>> _changed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
@@ -28,7 +28,7 @@ namespace Avalonia
             Action<AvaloniaObject, bool>? notifying = null)
             : base(name, typeof(TValue), ownerType, metadata, notifying)
         {
-            _changed = new Subject<AvaloniaPropertyChangedEventArgs<TValue>>();
+            _changed = new LightweightSubject<AvaloniaPropertyChangedEventArgs<TValue>>();
         }
 
         /// <summary>

+ 1 - 0
src/Avalonia.Base/ClassBindingManager.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Data;
+using Avalonia.Reactive;
 
 namespace Avalonia
 {

+ 1 - 1
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@@ -3,7 +3,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 
 namespace Avalonia.Collections
 {

+ 0 - 1
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Specialized;
-using System.Reactive.Linq;
 using Avalonia.Reactive;
 using Avalonia.Utilities;
 

+ 1 - 1
src/Avalonia.Base/Controls/NameScopeLocator.cs

@@ -1,5 +1,5 @@
 using System;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Utilities;
 
 namespace Avalonia.Controls

+ 5 - 6
src/Avalonia.Base/Data/BindingOperations.cs

@@ -1,6 +1,5 @@
 using System;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data
 {
@@ -46,15 +45,15 @@ namespace Avalonia.Data
                         throw new InvalidOperationException("InstancedBinding does not contain an observable.");
                     return target.Bind(property, binding.Observable, binding.Priority);
                 case BindingMode.TwoWay:
+                    if (binding.Observable is null)
+                        throw new InvalidOperationException("InstancedBinding does not contain an observable.");
                     if (binding.Subject is null)
                         throw new InvalidOperationException("InstancedBinding does not contain a subject.");
                     return new TwoWayBindingDisposable(
-                        target.Bind(property, binding.Subject, binding.Priority),
+                        target.Bind(property, binding.Observable, binding.Priority),
                         target.GetObservable(property).Subscribe(binding.Subject));
                 case BindingMode.OneTime:
-                    var source = binding.Subject ?? binding.Observable;
-
-                    if (source != null)
+                    if (binding.Observable is {} source)
                     {
                         // Perf: Avoid allocating closure in the outer scope.
                         var targetCopy = target;

+ 0 - 1
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Reactive.Linq;
 using Avalonia.Reactive;
 
 namespace Avalonia.Data.Core

+ 2 - 4
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -1,11 +1,9 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
+using Avalonia.Reactive;
 using Avalonia.Data.Converters;
 using Avalonia.Logging;
-using Avalonia.Reactive;
 using Avalonia.Utilities;
 
 namespace Avalonia.Data.Core
@@ -15,7 +13,7 @@ namespace Avalonia.Data.Core
     /// that are sent and received.
     /// </summary>
     [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
-    public class BindingExpression : LightweightObservableBase<object?>, ISubject<object?>, IDescription
+    public class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
     {
         private readonly ExpressionObserver _inner;
         private readonly Type _targetType;

+ 8 - 9
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@@ -2,8 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq.Expressions;
-using System.Reactive;
-using System.Reactive.Linq;
 using Avalonia.Data.Core.Parsers;
 using Avalonia.Data.Core.Plugins;
 using Avalonia.Reactive;
@@ -99,14 +97,14 @@ namespace Avalonia.Data.Core
         /// </summary>
         /// <param name="rootGetter">A function which gets the root object.</param>
         /// <param name="node">The expression.</param>
-        /// <param name="update">An observable which triggers a re-read of the getter.</param>
+        /// <param name="update">An observable which triggers a re-read of the getter. Generic argument value is not used.</param>
         /// <param name="description">
         /// A description of the expression.
         /// </param>
         public ExpressionObserver(
             Func<object?> rootGetter,
             ExpressionNode node,
-            IObservable<Unit> update,
+            IObservable<ValueTuple> update,
             string? description)
         {
             Description = description;
@@ -164,7 +162,7 @@ namespace Avalonia.Data.Core
         /// </summary>
         /// <param name="rootGetter">A function which gets the root object.</param>
         /// <param name="expression">The expression.</param>
-        /// <param name="update">An observable which triggers a re-read of the getter.</param>
+        /// <param name="update">An observable which triggers a re-read of the getter. Generic argument value is not used.</param>
         /// <param name="enableDataValidation">Whether or not to track data validation</param>
         /// <param name="description">
         /// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
@@ -173,7 +171,7 @@ namespace Avalonia.Data.Core
         public static ExpressionObserver Create<T, U>(
             Func<T> rootGetter,
             Expression<Func<T, U>> expression,
-            IObservable<Unit> update,
+            IObservable<ValueTuple> update,
             bool enableDataValidation = false,
             string? description = null)
         {
@@ -296,9 +294,10 @@ namespace Avalonia.Data.Core
             if (_root is IObservable<object> observable)
             {
                 _rootSubscription = observable.Subscribe(
-                    x => _node.Target = new WeakReference<object?>(x != AvaloniaProperty.UnsetValue ? x : null),
-                    x => PublishCompleted(),
-                    () => PublishCompleted());
+                    new AnonymousObserver<object>(
+                        x => _node.Target = new WeakReference<object?>(x != AvaloniaProperty.UnsetValue ? x : null),
+                        x => PublishCompleted(),
+                        PublishCompleted));
             }
             else
             {

+ 38 - 23
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@@ -1,48 +1,47 @@
 using System;
 using System.Collections;
-using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
-using System.Linq;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 using Avalonia.Utilities;
 
 namespace Avalonia.Data.Core
 {
-    public abstract class IndexerNodeBase : SettableNode
+    public abstract class IndexerNodeBase : SettableNode,
+        IWeakEventSubscriber<NotifyCollectionChangedEventArgs>,
+        IWeakEventSubscriber<PropertyChangedEventArgs>
     {
-        private IDisposable? _subscription;
-        
         protected override void StartListeningCore(WeakReference<object?> reference)
         {
             reference.TryGetTarget(out var target);
 
-            var incc = target as INotifyCollectionChanged;
-            var inpc = target as INotifyPropertyChanged;
-            var inputs = new List<IObservable<object?>>();
-
-            if (incc != null)
+            if (target is INotifyCollectionChanged incc)
             {
-                inputs.Add(WeakObservable.FromEventPattern(
-                    incc, WeakEvents.CollectionChanged)
-                    .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
-                    .Select(_ => GetValue(target)));
+                WeakEvents.CollectionChanged.Subscribe(incc, this);
             }
 
-            if (inpc != null)
+            if (target is INotifyPropertyChanged inpc)
             {
-                inputs.Add(WeakObservable.FromEventPattern(
-                    inpc, WeakEvents.PropertyChanged)
-                    .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
-                    .Select(_ => GetValue(target)));
+                WeakEvents.PropertyChanged.Subscribe(inpc, this);
             }
-
-            _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
+            
+            ValueChanged(GetValue(target));
         }
 
         protected override void StopListeningCore()
         {
-            _subscription?.Dispose();
+            if (Target.TryGetTarget(out var target))
+            {
+                if (target is INotifyCollectionChanged incc)
+                {
+                    WeakEvents.CollectionChanged.Unsubscribe(incc, this);
+                }
+
+                if (target is INotifyPropertyChanged inpc)
+                {
+                    WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
+                }
+            }
         }
 
         protected abstract object? GetValue(object? target);
@@ -83,5 +82,21 @@ namespace Avalonia.Data.Core
         }
 
         protected abstract bool ShouldUpdate(object? sender, PropertyChangedEventArgs e);
+
+        void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs e)
+        {
+            if (ShouldUpdate(sender, e))
+            {
+                ValueChanged(GetValue(sender));
+            }
+        }
+
+        void IWeakEventSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e)
+        {
+            if (ShouldUpdate(sender, e))
+            {
+                ValueChanged(GetValue(sender));
+            }
+        }
     }
 }

+ 24 - 41
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 using System.Reflection;
 
 namespace Avalonia.Data.Core.Plugins
@@ -12,8 +12,15 @@ namespace Avalonia.Data.Core.Plugins
     [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
     public class ObservableStreamPlugin : IStreamPlugin
     {
-        static MethodInfo? observableSelect;
+        private static MethodInfo? s_observableGeneric;
+        private static MethodInfo? s_observableSelect;
 
+        [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")]
+        public ObservableStreamPlugin()
+        {
+            
+        }
+        
         /// <summary>
         /// Checks whether this plugin handles the specified value.
         /// </summary>
@@ -54,56 +61,32 @@ namespace Avalonia.Data.Core.Plugins
                   x.IsGenericType &&
                   x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0];
 
-            // Get the Observable.Select method.
-            var select = GetObservableSelect(sourceType);
-
-            // Make a Box<> delegate of the correct type.
-            var funcType = typeof(Func<,>).MakeGenericType(sourceType, typeof(object));
-            var box = GetType().GetMethod(nameof(Box), BindingFlags.Static | BindingFlags.NonPublic)!
-                .MakeGenericMethod(sourceType)
-                .CreateDelegate(funcType);
+            // Get the BoxObservable<T> method.
+            var select = GetBoxObservable(sourceType);
 
-            // Call Observable.Select(target, box);
+            // Call BoxObservable(target);
             return (IObservable<object?>)select.Invoke(
                 null,
-                new object[] { target, box })!;
+                new[] { target })!;
         }
 
         [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
-        private static MethodInfo GetObservableSelect(Type source)
+        private static MethodInfo GetBoxObservable(Type source)
         {
-            return GetObservableSelect().MakeGenericMethod(source, typeof(object));
+            return (s_observableGeneric ??= GetBoxObservable()).MakeGenericMethod(source);
         }
 
-        private static MethodInfo GetObservableSelect()
+        [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
+        private static MethodInfo GetBoxObservable()
         {
-            if (observableSelect == null)
-            {
-                observableSelect = typeof(Observable).GetRuntimeMethods().First(x =>
-                {
-                    if (x.Name == nameof(Observable.Select) &&
-                        x.ContainsGenericParameters &&
-                        x.GetGenericArguments().Length == 2)
-                    {
-                        var parameters = x.GetParameters();
-
-                        if (parameters.Length == 2 &&
-                            parameters[0].ParameterType.IsConstructedGenericType &&
-                            parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IObservable<>) &&
-                            parameters[1].ParameterType.IsConstructedGenericType &&
-                            parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>))
-                        {
-                            return true;
-                        }
-                    }
-
-                    return false;
-                });
-            }
-
-            return observableSelect;
+            return s_observableSelect
+               ??= typeof(ObservableStreamPlugin).GetMethod(nameof(BoxObservable), BindingFlags.Static | BindingFlags.NonPublic)
+               ?? throw new InvalidOperationException("BoxObservable method was not found.");
         }
 
-        private static object? Box<T>(T value) => (object?)value;
+        private static IObservable<object?> BoxObservable<T>(IObservable<T> source)
+        {
+            return source.Select(v => (object?)v);
+        }
     }
 }

+ 2 - 3
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@@ -1,9 +1,8 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
 using System.Reflection;
 using System.Threading.Tasks;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data.Core.Plugins
 {
@@ -50,7 +49,7 @@ namespace Avalonia.Data.Core.Plugins
                         case TaskStatus.Faulted:
                             return HandleCompleted(task);
                         default:
-                            var subject = new Subject<object?>();
+                            var subject = new LightweightSubject<object?>();
                             task.ContinueWith(
                                     x => HandleCompleted(task).Subscribe(subject),
                                     TaskScheduler.FromCurrentSynchronizationContext())

+ 1 - 1
src/Avalonia.Base/Data/Core/StreamNode.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
-using System.Reactive.Linq;
 using Avalonia.Data.Core.Plugins;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data.Core
 {

+ 7 - 2
src/Avalonia.Base/Data/IndexerBinding.cs

@@ -1,4 +1,6 @@
-namespace Avalonia.Data
+using Avalonia.Reactive;
+
+namespace Avalonia.Data
 {
     public class IndexerBinding : IBinding
     {
@@ -22,7 +24,10 @@
             object? anchor = null,
             bool enableDataValidation = false)
         {
-            return new InstancedBinding(Source.GetSubject(Property), Mode, BindingPriority.LocalValue);
+            var subject = new CombinedSubject<object?>(
+                new AnonymousObserver<object?>(x => Source.SetValue(Property, x, BindingPriority.LocalValue)),
+                Source.GetObservable(Property));
+            return new InstancedBinding(subject, Mode, BindingPriority.LocalValue);
         }
     }
 }

+ 2 - 3
src/Avalonia.Base/Data/IndexerDescriptor.cs

@@ -1,12 +1,11 @@
 using System;
-using System.Reactive;
 
 namespace Avalonia.Data
 {
     /// <summary>
     /// Holds a description of a binding for <see cref="AvaloniaObject"/>'s [] operator.
     /// </summary>
-    public class IndexerDescriptor : ObservableBase<object?>, IDescription
+    public class IndexerDescriptor : IObservable<object?>, IDescription
     {
         /// <summary>
         /// Gets or sets the binding mode.
@@ -104,7 +103,7 @@ namespace Avalonia.Data
         }
 
         /// <inheritdoc/>
-        protected override IDisposable SubscribeCore(IObserver<object?> observer)
+        public IDisposable Subscribe(IObserver<object?> observer)
         {
             if (SourceObservable is null && Source is null)
                 throw new InvalidOperationException("Cannot subscribe to IndexerDescriptor.");

+ 20 - 30
src/Avalonia.Base/Data/InstancedBinding.cs

@@ -1,5 +1,5 @@
 using System;
-using System.Reactive.Subjects;
+using Avalonia.Reactive;
 
 namespace Avalonia.Data
 {
@@ -14,26 +14,7 @@ namespace Avalonia.Data
     /// </remarks>
     public class InstancedBinding
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
-        /// </summary>
-        /// <param name="subject">The binding source.</param>
-        /// <param name="mode">The binding mode.</param>
-        /// <param name="priority">The priority of the binding.</param>
-        /// <remarks>
-        /// This constructor can be used to create any type of binding and as such requires an
-        /// <see cref="ISubject{Object}"/> as the binding source because this is the only binding
-        /// source which can be used for all binding modes. If you wish to create an instance with
-        /// something other than a subject, use one of the static creation methods on this class.
-        /// </remarks>
-        public InstancedBinding(ISubject<object?> subject, BindingMode mode, BindingPriority priority)
-        {
-            Mode = mode;
-            Priority = priority;
-            Value = subject ?? throw new ArgumentNullException(nameof(subject));
-        }
-
-        private InstancedBinding(object? value, BindingMode mode, BindingPriority priority)
+        internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority)
         {
             Mode = mode;
             Priority = priority;
@@ -61,9 +42,14 @@ namespace Avalonia.Data
         public IObservable<object?>? Observable => Value as IObservable<object?>;
 
         /// <summary>
-        /// Gets the <see cref="Value"/> as a subject.
+        /// Gets the <see cref="Value"/> as an observer.
+        /// </summary>
+        public IObserver<object?>? Observer => Value as IObserver<object?>;
+
+        /// <summary>
+        /// Gets the <see cref="Subject"/> as an subject.
         /// </summary>
-        public ISubject<object?>? Subject => Value as ISubject<object?>;
+        internal IAvaloniaSubject<object?>? Subject => Value as IAvaloniaSubject<object?>;
 
         /// <summary>
         /// Creates a new one-time binding with a fixed value.
@@ -111,30 +97,34 @@ namespace Avalonia.Data
         /// <summary>
         /// Creates a new one-way to source binding.
         /// </summary>
-        /// <param name="subject">The binding source.</param>
+        /// <param name="observer">The binding source.</param>
         /// <param name="priority">The priority of the binding.</param>
         /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
         public static InstancedBinding OneWayToSource(
-            ISubject<object?> subject,
+            IObserver<object?> observer,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            _ = subject ?? throw new ArgumentNullException(nameof(subject));
+            _ = observer ?? throw new ArgumentNullException(nameof(observer));
 
-            return new InstancedBinding(subject, BindingMode.OneWayToSource, priority);
+            return new InstancedBinding(observer, BindingMode.OneWayToSource, priority);
         }
 
         /// <summary>
         /// Creates a new two-way binding.
         /// </summary>
-        /// <param name="subject">The binding source.</param>
+        /// <param name="observable">The binding source.</param>
+        /// <param name="observer">The binding source.</param>
         /// <param name="priority">The priority of the binding.</param>
         /// <returns>An <see cref="InstancedBinding"/> instance.</returns>
         public static InstancedBinding TwoWay(
-            ISubject<object?> subject,
+            IObservable<object?> observable,
+            IObserver<object?> observer,
             BindingPriority priority = BindingPriority.LocalValue)
         {
-            _ = subject ?? throw new ArgumentNullException(nameof(subject));
+            _ = observable ?? throw new ArgumentNullException(nameof(observable));
+            _ = observer ?? throw new ArgumentNullException(nameof(observer));
 
+            var subject = new CombinedSubject<object?>(observer, observable);
             return new InstancedBinding(subject, BindingMode.TwoWay, priority);
         }
 

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

@@ -3,6 +3,7 @@ using System.Threading;
 using Avalonia.Interactivity;
 using Avalonia.Platform;
 using Avalonia.Threading;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Input

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

@@ -7,6 +7,7 @@ using Avalonia.Data;
 using Avalonia.Input.GestureRecognizers;
 using Avalonia.Input.TextInput;
 using Avalonia.Interactivity;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 #nullable enable

+ 4 - 4
src/Avalonia.Base/Input/InputManager.cs

@@ -1,6 +1,6 @@
 using System;
-using System.Reactive.Subjects;
 using Avalonia.Input.Raw;
+using Avalonia.Reactive;
 
 namespace Avalonia.Input
 {
@@ -10,9 +10,9 @@ namespace Avalonia.Input
     /// </summary>
     public class InputManager : IInputManager
     {
-        private readonly Subject<RawInputEventArgs> _preProcess = new Subject<RawInputEventArgs>();
-        private readonly Subject<RawInputEventArgs> _process = new Subject<RawInputEventArgs>();
-        private readonly Subject<RawInputEventArgs> _postProcess = new Subject<RawInputEventArgs>();
+        private readonly LightweightSubject<RawInputEventArgs> _preProcess = new();
+        private readonly LightweightSubject<RawInputEventArgs> _process = new();
+        private readonly LightweightSubject<RawInputEventArgs> _postProcess = new();
 
         /// <summary>
         /// Gets the global instance of the input manager.

+ 1 - 0
src/Avalonia.Base/Input/MouseDevice.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using Avalonia.Reactive;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Utilities;

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

@@ -1,5 +1,5 @@
 using System;
-using Avalonia.VisualTree;
+using Avalonia.Reactive;
 
 namespace Avalonia.Input.TextInput
 {

+ 1 - 2
src/Avalonia.Base/Interactivity/InteractiveExtensions.cs

@@ -1,6 +1,5 @@
 using System;
-using System.Reactive.Disposables;
-using System.Reactive.Linq;
+using Avalonia.Reactive;
 
 namespace Avalonia.Interactivity
 {

+ 3 - 3
src/Avalonia.Base/Interactivity/RoutedEvent.cs

@@ -1,5 +1,5 @@
 using System;
-using System.Reactive.Subjects;
+using Avalonia.Reactive;
 
 namespace Avalonia.Interactivity
 {
@@ -13,8 +13,8 @@ namespace Avalonia.Interactivity
 
     public class RoutedEvent
     {
-        private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>();
-        private readonly Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
+        private readonly LightweightSubject<(object, RoutedEventArgs)> _raised = new();
+        private readonly LightweightSubject<RoutedEventArgs> _routeFinished = new();
 
         public RoutedEvent(
             string name,

+ 7 - 11
src/Avalonia.Base/Layout/Layoutable.cs

@@ -1,6 +1,6 @@
 using System;
 using Avalonia.Logging;
-using Avalonia.Styling;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 #nullable enable
@@ -470,14 +470,12 @@ namespace Avalonia.Layout
         protected static void AffectsMeasure<T>(params AvaloniaProperty[] properties)
             where T : Layoutable
         {
-            void Invalidate(AvaloniaPropertyChangedEventArgs e)
-            {
-                (e.Sender as T)?.InvalidateMeasure();
-            }
+            var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e => (e.Sender as T)?.InvalidateMeasure());
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(Invalidate);
+                property.Changed.Subscribe(invalidateObserver);
             }
         }
 
@@ -493,14 +491,12 @@ namespace Avalonia.Layout
         protected static void AffectsArrange<T>(params AvaloniaProperty[] properties)
             where T : Layoutable
         {
-            void Invalidate(AvaloniaPropertyChangedEventArgs e)
-            {
-                (e.Sender as T)?.InvalidateArrange();
-            }
+            var invalidate = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e => (e.Sender as T)?.InvalidateArrange());
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(Invalidate);
+                property.Changed.Subscribe(invalidate);
             }
         }
 

+ 4 - 5
src/Avalonia.Base/Media/Brush.cs

@@ -3,6 +3,7 @@ using System.ComponentModel;
 using Avalonia.Animation;
 using Avalonia.Animation.Animators;
 using Avalonia.Media.Immutable;
+using Avalonia.Reactive;
 
 namespace Avalonia.Media
 {
@@ -103,14 +104,12 @@ namespace Avalonia.Media
         protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
             where T : Brush
         {
-            static void Invalidate(AvaloniaPropertyChangedEventArgs e)
-            {
-                (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty);
-            }
+            var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(e => Invalidate(e));
+                property.Changed.Subscribe(invalidateObserver);
             }
         }
 

+ 5 - 6
src/Avalonia.Base/Media/DashStyle.cs

@@ -4,6 +4,7 @@ using System.Collections.Specialized;
 using Avalonia.Animation;
 using Avalonia.Collections;
 using Avalonia.Media.Immutable;
+using Avalonia.Reactive;
 
 #nullable enable
 
@@ -51,13 +52,11 @@ namespace Avalonia.Media
 
         static DashStyle()
         {
-            void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e)
-            {
-                ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty);
-            }
+            var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e => ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty));
 
-            DashesProperty.Changed.Subscribe(RaiseInvalidated);
-            OffsetProperty.Changed.Subscribe(RaiseInvalidated);
+            DashesProperty.Changed.Subscribe(invalidateObserver);
+            OffsetProperty.Changed.Subscribe(invalidateObserver);
         }
 
         /// <summary>

+ 4 - 5
src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 
 namespace Avalonia.Media
 {
@@ -274,14 +275,12 @@ namespace Avalonia.Media
         protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
             where T : ExperimentalAcrylicMaterial
         {
-            static void Invalidate(AvaloniaPropertyChangedEventArgs e)
-            {
-                (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty);
-            }
+            var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
 
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(e => Invalidate(e));
+                property.Changed.Subscribe(invalidateObserver);
             }
         }
 

+ 3 - 1
src/Avalonia.Base/Media/Geometry.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Platform;
+using Avalonia.Reactive;
 
 namespace Avalonia.Media
 {
@@ -117,9 +118,10 @@ namespace Avalonia.Media
         /// </remarks>
         protected static void AffectsGeometry(params AvaloniaProperty[] properties)
         {
+            var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(AffectsGeometryInvalidate);
             foreach (var property in properties)
             {
-                property.Changed.Subscribe(AffectsGeometryInvalidate);
+                property.Changed.Subscribe(invalidateObserver);
             }
         }
 

+ 1 - 0
src/Avalonia.Base/Media/GradientBrush.cs

@@ -6,6 +6,7 @@ using System.ComponentModel;
 using Avalonia.Animation.Animators;
 using Avalonia.Collections;
 using Avalonia.Metadata;
+using Avalonia.Reactive;
 
 namespace Avalonia.Media
 {

+ 1 - 0
src/Avalonia.Base/Media/MatrixTransform.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media

+ 1 - 0
src/Avalonia.Base/Media/RotateTransform.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media

+ 11 - 2
src/Avalonia.Base/Media/ScaleTransform.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media
@@ -25,8 +26,6 @@ namespace Avalonia.Media
         /// </summary>
         public ScaleTransform()
         {
-            this.GetObservable(ScaleXProperty).Subscribe(_ => RaiseChanged());
-            this.GetObservable(ScaleYProperty).Subscribe(_ => RaiseChanged());
         }
 
         /// <summary>
@@ -63,5 +62,15 @@ namespace Avalonia.Media
         /// Gets the transform's <see cref="Matrix"/>.
         /// </summary>
         public override Matrix Value => Matrix.CreateScale(ScaleX, ScaleY);
+        
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == ScaleXProperty || change.Property == ScaleYProperty)
+            {
+                RaiseChanged();
+            }
+        }
     }
 }

+ 11 - 2
src/Avalonia.Base/Media/SkewTransform.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media
@@ -25,8 +26,6 @@ namespace Avalonia.Media
         /// </summary>
         public SkewTransform()
         {
-            this.GetObservable(AngleXProperty).Subscribe(_ => RaiseChanged());
-            this.GetObservable(AngleYProperty).Subscribe(_ => RaiseChanged());
         }
 
         /// <summary>
@@ -62,5 +61,15 @@ namespace Avalonia.Media
         /// Gets the transform's <see cref="Matrix"/>.
         /// </summary>
         public override Matrix Value => Matrix.CreateSkew(Matrix.ToRadians(AngleX), Matrix.ToRadians(AngleY));
+        
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == AngleXProperty || change.Property == AngleYProperty)
+            {
+                RaiseChanged();
+            }
+        }
     }
 }

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

@@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
                     throw new ArgumentOutOfRangeException(nameof(index));
                 }
 #endif
-                return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
+                return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
             }
         }
 
@@ -157,8 +157,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Gets a span from the character buffer range
         /// </summary>
-        public ReadOnlySpan<char> Span =>
-            CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
+        public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
 
         /// <summary>
         /// Gets the character memory buffer
@@ -174,7 +173,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Indicate whether the character buffer range is empty
         /// </summary>
-        internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0;
+        internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
 
         internal CharacterBufferRange Take(int length)
         {
@@ -208,9 +207,7 @@ namespace Avalonia.Media.TextFormatting
                 return new CharacterBufferRange(new CharacterBufferReference(), 0);
             }
 
-            var characterBufferReference = new CharacterBufferReference(
-                CharacterBufferReference.CharacterBuffer,
-                CharacterBufferReference.OffsetToFirstChar + length);
+            var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
 
             return new CharacterBufferRange(characterBufferReference, Length - length);
         }

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

@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
         /// Collapses given text line.
         /// </summary>
         /// <param name="textLine">Text line to collapse.</param>
-        public abstract List<DrawableTextRun>? Collapse(TextLine textLine);
+        public abstract List<TextRun>? Collapse(TextLine textLine);
     }
 }

+ 13 - 10
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting
 {
     internal static class TextEllipsisHelper
     {
-        public static List<DrawableTextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+        public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
         {
-            if (textLine.TextRuns is not List<DrawableTextRun> textRuns || textRuns.Count == 0)
+            var textRuns = textLine.TextRuns;
+
+            if (textRuns == null || textRuns.Count == 0)
             {
                 return null;
             }
@@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting
             if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
             {
                 //Not enough space to fit in the symbol
-                return new List<DrawableTextRun>(0);
+                return new List<TextRun>(0);
             }
 
             var availableWidth = properties.Width - shapedSymbol.Size.Width;
@@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
 
                                 collapsedLength += measuredLength;
 
-                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                                var collapsedRuns = new List<TextRun>(textRuns.Count);
 
                                 if (collapsedLength > 0)
                                 {
-                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                    var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
 
                                     collapsedRuns.AddRange(splitResult.First);
                                 }
@@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting
                                 return collapsedRuns;
                             }
 
-                            availableWidth -= currentRun.Size.Width;
-
+                            availableWidth -= shapedRun.Size.Width;
 
                             break;
                         }
 
-                    case { } drawableRun:
+                    case DrawableTextRun drawableRun:
                         {
                             //The whole run needs to fit into available space
                             if (currentWidth + drawableRun.Size.Width > availableWidth)
                             {
-                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                                var collapsedRuns = new List<TextRun>(textRuns.Count);
 
                                 if (collapsedLength > 0)
                                 {
-                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                    var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
 
                                     collapsedRuns.AddRange(splitResult.First);
                                 }
@@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting
                                 return collapsedRuns;
                             }
 
+                            availableWidth -= drawableRun.Size.Width;
+
                             break;
                         }
                 }

+ 117 - 101
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
 
@@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting
             var textWrapping = paragraphProperties.TextWrapping;
             FlowDirection resolvedFlowDirection;
             TextLineBreak? nextLineBreak = null;
-            List<DrawableTextRun> drawableTextRuns;
+            IReadOnlyList<TextRun> textRuns;
 
-            var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
+            var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex,
                 out var textEndOfLine, out var textSourceLength);
 
             if (previousLineBreak?.RemainingRuns != null)
             {
                 resolvedFlowDirection = previousLineBreak.FlowDirection;
-                drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
+                textRuns = previousLineBreak.RemainingRuns;
                 nextLineBreak = previousLineBreak;
             }
             else
             {
-                drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection);
+                textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection);
 
                 if (nextLineBreak == null && textEndOfLine != null)
                 {
@@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 case TextWrapping.NoWrap:
                     {
-                        textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
+                        textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength,
                             paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
                         textLine.FinalizeLine();
@@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting
                 case TextWrapping.WrapWithOverflow:
                 case TextWrapping.Wrap:
                     {
-                        textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
+                        textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
                             resolvedFlowDirection, nextLineBreak);
                         break;
                     }
@@ -71,7 +70,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textRuns">The text run's.</param>
         /// <param name="length">The length to split at.</param>
         /// <returns>The split text runs.</returns>
-        internal static SplitResult<List<DrawableTextRun>> SplitDrawableRuns(List<DrawableTextRun> textRuns, int length)
+        internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length)
         {
             var currentLength = 0;
 
@@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var firstCount = currentRun.Length >= 1 ? i + 1 : i;
 
-                var first = new List<DrawableTextRun>(firstCount);
+                var first = new List<TextRun>(firstCount);
 
                 if (firstCount > 1)
                 {
@@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentLength + currentRun.Length == length)
                 {
-                    var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
+                    var second = secondCount > 0 ? new List<TextRun>(secondCount) : null;
 
                     if (second != null)
                     {
@@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting
 
                     first.Add(currentRun);
 
-                    return new SplitResult<List<DrawableTextRun>>(first, second);
+                    return new SplitResult<IReadOnlyList<TextRun>>(first, second);
                 }
                 else
                 {
                     secondCount++;
 
-                    var second = new List<DrawableTextRun>(secondCount);
+                    var second = new List<TextRun>(secondCount);
 
                     if (currentRun is ShapedTextRun shapedTextCharacters)
                     {
@@ -131,18 +130,18 @@ namespace Avalonia.Media.TextFormatting
                         first.Add(split.First);
 
                         second.Add(split.Second!);
-                    }                
+                    }
 
                     for (var j = 1; j < secondCount; j++)
                     {
                         second.Add(textRuns[i + j]);
                     }
 
-                    return new SplitResult<List<DrawableTextRun>>(first, second);
+                    return new SplitResult<IReadOnlyList<TextRun>>(first, second);
                 }
             }
 
-            return new SplitResult<List<DrawableTextRun>>(textRuns, null);
+            return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null);
         }
 
         /// <summary>
@@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>
         /// A list of shaped text characters.
         /// </returns>
-        private static List<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
+        private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
             out FlowDirection resolvedFlowDirection)
         {
             var flowDirection = paragraphProperties.FlowDirection;
-            var drawableTextRuns = new List<DrawableTextRun>();
+            var shapedRuns = new List<TextRun>();
             var biDiData = new BidiData((sbyte)flowDirection);
 
             foreach (var textRun in textRuns)
@@ -199,13 +198,6 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case DrawableTextRun drawableRun:
-                        {
-                            drawableTextRuns.Add(drawableRun);
-
-                            break;
-                        }
-
                     case UnshapedTextRun shapeableRun:
                         {
                             var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
@@ -245,17 +237,23 @@ namespace Avalonia.Media.TextFormatting
 
                             var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
                                         currentRun.Properties.FontRenderingEmSize,
-                                         shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, 
+                                         shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
                                          paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
 
-                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
+                            shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
+
+                            break;
+                        }
+                    default:
+                        {
+                            shapedRuns.Add(currentRun);
 
                             break;
                         }
                 }
             }
 
-            return drawableTextRuns;
+            return shapedRuns;
         }
 
         private static IReadOnlyList<ShapedTextRun> ShapeTogether(
@@ -390,6 +388,10 @@ namespace Avalonia.Media.TextFormatting
 
                 if (textRun == null)
                 {
+                    textRuns.Add(new TextEndOfParagraph());
+
+                    textSourceLength += TextRun.DefaultTextSourceLength;
+
                     break;
                 }
 
@@ -465,7 +467,7 @@ namespace Avalonia.Media.TextFormatting
             return false;
         }
 
-        private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength)
+        private static bool TryMeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth, out int measuredLength)
         {
             measuredLength = 0;
             var currentWidth = 0.0;
@@ -476,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextRun shapedTextCharacters:
                         {
-                            if(shapedTextCharacters.ShapedBuffer.Length > 0)
+                            if (shapedTextCharacters.ShapedBuffer.Length > 0)
                             {
                                 var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
                                 var lastCluster = firstCluster;
@@ -497,12 +499,12 @@ namespace Avalonia.Media.TextFormatting
                                 }
 
                                 measuredLength += currentRun.Length;
-                            }                         
+                            }
 
                             break;
                         }
 
-                    case { } drawableTextRun:
+                    case DrawableTextRun drawableTextRun:
                         {
                             if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
                             {
@@ -510,14 +512,20 @@ namespace Avalonia.Media.TextFormatting
                             }
 
                             measuredLength += currentRun.Length;
-                            currentWidth += currentRun.Size.Width;
+                            currentWidth += drawableTextRun.Size.Width;
+
+                            break;
+                        }
+                    default:
+                        {
+                            measuredLength += currentRun.Length;
 
                             break;
                         }
                 }
             }
 
-            found:
+        found:
 
             return measuredLength != 0;
         }
@@ -553,13 +561,13 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="resolvedFlowDirection"></param>
         /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
         /// <returns>The wrapped text line.</returns>
-        private static TextLineImpl PerformTextWrapping(List<DrawableTextRun> textRuns, int firstTextSourceIndex,
+        private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
             double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
             TextLineBreak? currentLineBreak)
         {
-            if(textRuns.Count == 0)
+            if (textRuns.Count == 0)
             {
-                return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties);
+                return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
             }
 
             if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@@ -575,46 +583,24 @@ namespace Avalonia.Media.TextFormatting
 
             for (var index = 0; index < textRuns.Count; index++)
             {
-                var currentRun = textRuns[index];
-
-                var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
-
-                var lineBreaker = new LineBreakEnumerator(runText);
-
                 var breakFound = false;
 
-                while (lineBreaker.MoveNext())
-                {
-                    if (lineBreaker.Current.Required &&
-                        currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
-                    {
-                        //Explicit break found
-                        breakFound = true;
-
-                        currentPosition = currentLength + lineBreaker.Current.PositionWrap;
-
-                        break;
-                    }
+                var currentRun = textRuns[index];
 
-                    if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
-                    {
-                        if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
+                switch (currentRun)
+                {
+                    case ShapedTextRun:
                         {
-                            if (lastWrapPosition > 0)
-                            {
-                                currentPosition = lastWrapPosition;
+                            var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
 
-                                breakFound = true;
+                            var lineBreaker = new LineBreakEnumerator(runText);
 
-                                break;
-                            }
-
-                            //Find next possible wrap position (overflow)
-                            if (index < textRuns.Count - 1)
+                            while (lineBreaker.MoveNext())
                             {
-                                if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                if (lineBreaker.Current.Required &&
+                                    currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
                                 {
-                                    //We already found the next possible wrap position.
+                                    //Explicit break found
                                     breakFound = true;
 
                                     currentPosition = currentLength + lineBreaker.Current.PositionWrap;
@@ -622,51 +608,81 @@ namespace Avalonia.Media.TextFormatting
                                     break;
                                 }
 
-                                while (lineBreaker.MoveNext() && index < textRuns.Count)
+                                if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
                                 {
-                                    currentPosition += lineBreaker.Current.PositionWrap;
-
-                                    if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                    if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
                                     {
-                                        break;
-                                    }
+                                        if (lastWrapPosition > 0)
+                                        {
+                                            currentPosition = lastWrapPosition;
 
-                                    index++;
+                                            breakFound = true;
+
+                                            break;
+                                        }
+
+                                        //Find next possible wrap position (overflow)
+                                        if (index < textRuns.Count - 1)
+                                        {
+                                            if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                            {
+                                                //We already found the next possible wrap position.
+                                                breakFound = true;
+
+                                                currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+
+                                                break;
+                                            }
+
+                                            while (lineBreaker.MoveNext() && index < textRuns.Count)
+                                            {
+                                                currentPosition += lineBreaker.Current.PositionWrap;
+
+                                                if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                                {
+                                                    break;
+                                                }
+
+                                                index++;
+
+                                                if (index >= textRuns.Count)
+                                                {
+                                                    break;
+                                                }
+
+                                                currentRun = textRuns[index];
+
+                                                runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+
+                                                lineBreaker = new LineBreakEnumerator(runText);
+                                            }
+                                        }
+                                        else
+                                        {
+                                            currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+                                        }
+
+                                        breakFound = true;
 
-                                    if (index >= textRuns.Count)
-                                    {
                                         break;
                                     }
 
-                                    currentRun = textRuns[index];
+                                    //We overflowed so we use the last available wrap position.
+                                    currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
 
-                                    runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+                                    breakFound = true;
 
-                                    lineBreaker = new LineBreakEnumerator(runText);
+                                    break;
                                 }
-                            }
-                            else
-                            {
-                                currentPosition = currentLength + lineBreaker.Current.PositionWrap;
-                            }
 
-                            breakFound = true;
+                                if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
+                                {
+                                    lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
+                                }
+                            }
 
                             break;
                         }
-
-                        //We overflowed so we use the last available wrap position.
-                        currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
-
-                        breakFound = true;
-
-                        break;
-                    }
-
-                    if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
-                    {
-                        lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
-                    }
                 }
 
                 if (!breakFound)
@@ -681,12 +697,12 @@ namespace Avalonia.Media.TextFormatting
                 break;
             }
 
-            var splitResult = SplitDrawableRuns(textRuns, measuredLength);
+            var splitResult = SplitTextRuns(textRuns, measuredLength);
 
             var remainingCharacters = splitResult.Second;
 
             var lineBreak = remainingCharacters?.Count > 0 ?
-                new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) :
+                new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) :
                 null;
 
             if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)

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

@@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
                 var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
                     _paragraphProperties, previousLine?.TextLineBreak);
 
-                if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                if(textLine == null || textLine.Length == 0)
                 {
                     if (previousLine != null && previousLine.NewLineLength > 0)
                     {
@@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting
 
                     break;
                 }
+
+                if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                {
+                    break;
+                }
             }
 
             //Make sure the TextLayout always contains at least on empty line

+ 72 - 61
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<DrawableTextRun>? Collapse(TextLine textLine)
+        public override List<TextRun>? Collapse(TextLine textLine)
         {
-            if (textLine.TextRuns is not List<DrawableTextRun> textRuns || textRuns.Count == 0)
+            var textRuns = textLine.TextRuns;
+
+            if (textRuns == null || textRuns.Count == 0)
             {
                 return null;
             }
@@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting
 
             if (Width < shapedSymbol.GlyphRun.Size.Width)
             {
-                return new List<DrawableTextRun>(0);
+                return new List<TextRun>(0);
             }
 
             // Overview of ellipsis structure
@@ -66,92 +68,101 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case ShapedTextRun shapedRun:
-                    {
-                        currentWidth += currentRun.Size.Width;
-
-                        if (currentWidth > availableWidth)
                         {
-                            shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
-
-                            var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                            currentWidth += shapedRun.Size.Width;
 
-                            if (measuredLength > 0)
+                            if (currentWidth > availableWidth)
                             {
-                                List<DrawableTextRun>? preSplitRuns = null;
-                                List<DrawableTextRun>? postSplitRuns;
-
-                                if (_prefixLength > 0)
-                                {
-                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
-                                        Math.Min(_prefixLength, measuredLength));
+                                shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
 
-                                    collapsedRuns.AddRange(splitResult.First);
+                                var collapsedRuns = new List<TextRun>(textRuns.Count);
 
-                                    preSplitRuns = splitResult.First;
-                                    postSplitRuns = splitResult.Second;
-                                }
-                                else
+                                if (measuredLength > 0)
                                 {
-                                    postSplitRuns = textRuns;
-                                }
+                                    IReadOnlyList<TextRun>? preSplitRuns = null;
+                                    IReadOnlyList<TextRun>? postSplitRuns;
 
-                                collapsedRuns.Add(shapedSymbol);
+                                    if (_prefixLength > 0)
+                                    {
+                                        var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
+                                            Math.Min(_prefixLength, measuredLength));
 
-                                if (measuredLength <= _prefixLength || postSplitRuns is null)
-                                {
-                                    return collapsedRuns;
-                                }
+                                        collapsedRuns.AddRange(splitResult.First);
 
-                                var availableSuffixWidth = availableWidth;
+                                        preSplitRuns = splitResult.First;
+                                        postSplitRuns = splitResult.Second;
+                                    }
+                                    else
+                                    {
+                                        postSplitRuns = textRuns;
+                                    }
 
-                                if (preSplitRuns is not null)
-                                {
-                                    foreach (var run in preSplitRuns)
+                                    collapsedRuns.Add(shapedSymbol);
+
+                                    if (measuredLength <= _prefixLength || postSplitRuns is null)
                                     {
-                                        availableSuffixWidth -= run.Size.Width;
+                                        return collapsedRuns;
                                     }
-                                }
 
-                                for (var i = postSplitRuns.Count - 1; i >= 0; i--)
-                                {
-                                    var run = postSplitRuns[i];
+                                    var availableSuffixWidth = availableWidth;
 
-                                    switch (run)
+                                    if (preSplitRuns is not null)
                                     {
-                                        case ShapedTextRun endShapedRun:
+                                        foreach (var run in preSplitRuns)
                                         {
-                                            if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
-                                                    out var suffixCount, out var suffixWidth))
+                                            if (run is DrawableTextRun drawableTextRun)
                                             {
-                                                availableSuffixWidth -= suffixWidth;
+                                                availableSuffixWidth -= drawableTextRun.Size.Width;
+                                            }
+                                        }
+                                    }
 
-                                                if (suffixCount > 0)
+                                    for (var i = postSplitRuns.Count - 1; i >= 0; i--)
+                                    {
+                                        var run = postSplitRuns[i];
+
+                                        switch (run)
+                                        {
+                                            case ShapedTextRun endShapedRun:
                                                 {
-                                                    var splitSuffix =
-                                                        endShapedRun.Split(run.Length - suffixCount);
+                                                    if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
+                                                            out var suffixCount, out var suffixWidth))
+                                                    {
+                                                        availableSuffixWidth -= suffixWidth;
 
-                                                    collapsedRuns.Add(splitSuffix.Second!);
-                                                }
-                                            }
+                                                        if (suffixCount > 0)
+                                                        {
+                                                            var splitSuffix =
+                                                                endShapedRun.Split(run.Length - suffixCount);
+
+                                                            collapsedRuns.Add(splitSuffix.Second!);
+                                                        }
+                                                    }
 
-                                            break;
+                                                    break;
+                                                }
                                         }
                                     }
                                 }
-                            }
-                            else
-                            {
-                                collapsedRuns.Add(shapedSymbol);
+                                else
+                                {
+                                    collapsedRuns.Add(shapedSymbol);
+                                }
+
+                                return collapsedRuns;
                             }
 
-                            return collapsedRuns;
-                        }
+                            availableWidth -= shapedRun.Size.Width;
 
-                        break;
-                    }
-                }
+                            break;
+                        }
+                    case DrawableTextRun drawableTextRun:
+                        {
+                            availableWidth -= drawableTextRun.Size.Width;
 
-                availableWidth -= currentRun.Size.Width;
+                            break;
+                        }
+                }            
 
                 runIndex++;
             }

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

@@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
     public class TextLineBreak
     {
         public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, 
-            IReadOnlyList<DrawableTextRun>? remainingRuns = null)
+            IReadOnlyList<TextRun>? remainingRuns = null)
         {
             TextEndOfLine = textEndOfLine;
             FlowDirection = flowDirection;
@@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Get the remaining runs that were split up by the <see cref="TextFormatter"/> during the formatting process.
         /// </summary>
-        public IReadOnlyList<DrawableTextRun>? RemainingRuns { get; }
+        public IReadOnlyList<TextRun>? RemainingRuns { get; }
     }
 }

+ 76 - 30
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -6,13 +6,13 @@ namespace Avalonia.Media.TextFormatting
 {
     internal class TextLineImpl : TextLine
     {
-        private readonly List<DrawableTextRun> _textRuns;
+        private IReadOnlyList<TextRun> _textRuns;
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
         private readonly FlowDirection _resolvedFlowDirection;
 
-        public TextLineImpl(List<DrawableTextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+        public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
             TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
             TextLineBreak? lineBreak = null, bool hasCollapsed = false)
         {
@@ -86,11 +86,14 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textRun in _textRuns)
             {
-                var offsetY = GetBaselineOffset(this, textRun);
+                if (textRun is DrawableTextRun drawable)
+                {
+                    var offsetY = GetBaselineOffset(this, drawable);
 
-                textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
+                    drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY));
 
-                currentX += textRun.Size.Width;
+                    currentX += drawable.Size.Width;
+                }
             }
         }
 
@@ -180,7 +183,14 @@ namespace Avalonia.Media.TextFormatting
             {
                 var lastRun = _textRuns[_textRuns.Count - 1];
 
-                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
+                var size = 0.0;
+
+                if (lastRun is DrawableTextRun drawableTextRun)
+                {
+                    size = drawableTextRun.Size.Width;
+                }
+
+                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
             }
 
             // process hit that happens within the line
@@ -220,9 +230,16 @@ namespace Avalonia.Media.TextFormatting
 
                         currentRun = _textRuns[j];
 
-                        if (currentDistance + currentRun.Size.Width <= distance)
+                        if(currentRun is not ShapedTextRun)
+                        {
+                            continue;
+                        }
+
+                        shapedRun = (ShapedTextRun)currentRun;
+
+                        if (currentDistance + shapedRun.Size.Width <= distance)
                         {
-                            currentDistance += currentRun.Size.Width;
+                            currentDistance += shapedRun.Size.Width;
                             currentPosition -= currentRun.Length;
 
                             continue;
@@ -234,12 +251,19 @@ namespace Avalonia.Media.TextFormatting
 
                 characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
 
-                if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
+                if (currentRun is DrawableTextRun drawableTextRun)
                 {
-                    currentDistance += currentRun.Size.Width;
+                    if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance)
+                    {
+                        currentDistance += drawableTextRun.Size.Width;
 
-                    currentPosition += currentRun.Length;
+                        currentPosition += currentRun.Length;
 
+                        continue;
+                    }
+                }
+                else
+                {
                     continue;
                 }
 
@@ -249,7 +273,7 @@ namespace Avalonia.Media.TextFormatting
             return characterHit;
         }
 
-        private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance)
+        private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance)
         {
             CharacterHit characterHit;
 
@@ -270,9 +294,9 @@ namespace Avalonia.Media.TextFormatting
 
                         break;
                     }
-                default:
+                case DrawableTextRun drawableTextRun:
                     {
-                        if (distance < run.Size.Width / 2)
+                        if (distance < drawableTextRun.Size.Width / 2)
                         {
                             characterHit = new CharacterHit(currentPosition);
                         }
@@ -282,6 +306,10 @@ namespace Avalonia.Media.TextFormatting
                         }
                         break;
                     }
+                default:
+                    characterHit = new CharacterHit(currentPosition, run.Length);
+
+                    break;
             }
 
             return characterHit;
@@ -307,7 +335,7 @@ namespace Avalonia.Media.TextFormatting
                     {
                         var i = index;
 
-                        var rightToLeftWidth = currentRun.Size.Width;
+                        var rightToLeftWidth = shapedRun.Size.Width;
 
                         while (i + 1 <= _textRuns.Count - 1)
                         {
@@ -317,7 +345,7 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 i++;
 
-                                rightToLeftWidth += nextRun.Size.Width;
+                                rightToLeftWidth += nextShapedRun.Size.Width;
 
                                 continue;
                             }
@@ -331,7 +359,10 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 currentRun = _textRuns[i];
 
-                                rightToLeftWidth -= currentRun.Size.Width;
+                                if (currentRun is DrawableTextRun drawable)
+                                {
+                                    rightToLeftWidth -= drawable.Size.Width;
+                                }
 
                                 if (currentPosition + currentRun.Length >= characterIndex)
                                 {
@@ -355,8 +386,13 @@ namespace Avalonia.Media.TextFormatting
                         return Math.Max(0, currentDistance + distance);
                     }
 
+                    if (currentRun is DrawableTextRun drawableTextRun)
+                    {
+                        currentDistance += drawableTextRun.Size.Width;
+                    }
+
                     //No hit hit found so we add the full width
-                    currentDistance += currentRun.Size.Width;
+
                     currentPosition += currentRun.Length;
                     remainingLength -= currentRun.Length;
                 }
@@ -380,8 +416,12 @@ namespace Avalonia.Media.TextFormatting
                         return Math.Max(0, currentDistance - distance);
                     }
 
+                    if (currentRun is DrawableTextRun drawableTextRun)
+                    {
+                        currentDistance -= drawableTextRun.Size.Width;
+                    }
+
                     //No hit hit found so we add the full width
-                    currentDistance -= currentRun.Size.Width;
                     currentPosition += currentRun.Length;
                     remainingLength -= currentRun.Length;
                 }
@@ -391,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
         }
 
         private static bool TryGetDistanceFromCharacterHit(
-            DrawableTextRun currentRun,
+            TextRun currentRun,
             CharacterHit characterHit,
             int currentPosition,
             int remainingLength,
@@ -432,7 +472,7 @@ namespace Avalonia.Media.TextFormatting
 
                         break;
                     }
-                default:
+                case DrawableTextRun drawableTextRun:
                     {
                         if (characterIndex == currentPosition)
                         {
@@ -441,7 +481,7 @@ namespace Avalonia.Media.TextFormatting
 
                         if (characterIndex == currentPosition + currentRun.Length)
                         {
-                            distance = currentRun.Size.Width;
+                            distance = drawableTextRun.Size.Width;
 
                             return true;
 
@@ -449,6 +489,10 @@ namespace Avalonia.Media.TextFormatting
 
                         break;
                     }
+                default:
+                    {
+                        return false;
+                    }
             }
 
             return false;
@@ -943,7 +987,7 @@ namespace Avalonia.Media.TextFormatting
             return this;
         }
 
-        private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
+        private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
         {
             if (run is ShapedTextRun shapedTextCharacters)
             {
@@ -1039,16 +1083,18 @@ namespace Avalonia.Media.TextFormatting
                 minLevelToReverse--;
             }
 
-            _textRuns.Clear();
+            var textRuns = new List<TextRun>(_textRuns.Count);
 
             current = orderedRun;
 
             while (current != null)
             {
-                _textRuns.Add(current.Run);
+                textRuns.Add(current.Run);
 
                 current = current.Next;
             }
+
+            _textRuns = textRuns;
         }
 
         /// <summary>
@@ -1286,7 +1332,7 @@ namespace Avalonia.Media.TextFormatting
         {
             var runIndex = 0;
             textPosition = FirstTextSourceIndex;
-            DrawableTextRun? previousRun = null;
+            TextRun? previousRun = null;
 
             while (runIndex < _textRuns.Count)
             {
@@ -1346,7 +1392,6 @@ namespace Avalonia.Media.TextFormatting
 
                             break;
                         }
-
                     default:
                         {
                             if (codepointIndex == textPosition)
@@ -1363,6 +1408,7 @@ namespace Avalonia.Media.TextFormatting
 
                             break;
                         }
+
                 }
 
                 runIndex++;
@@ -1436,7 +1482,7 @@ namespace Avalonia.Media.TextFormatting
                             break;
                         }
 
-                    case { } drawableTextRun:
+                    case DrawableTextRun drawableTextRun:
                         {
                             widthIncludingWhitespace += drawableTextRun.Size.Width;
 
@@ -1558,7 +1604,7 @@ namespace Avalonia.Media.TextFormatting
 
         private sealed class OrderedBidiRun
         {
-            public OrderedBidiRun(DrawableTextRun run, sbyte level)
+            public OrderedBidiRun(TextRun run, sbyte level)
             {
                 Run = run;
                 Level = level;
@@ -1566,7 +1612,7 @@ namespace Avalonia.Media.TextFormatting
 
             public sbyte Level { get; }
 
-            public DrawableTextRun Run { get; }
+            public TextRun Run { get; }
 
             public OrderedBidiRun? Next { get; set; }
         }

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

@@ -40,11 +40,11 @@ namespace Avalonia.Media.TextFormatting
                 {
                     unsafe
                     {
-                        var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
+                        var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
 
                         fixed (char* charsPtr = characterBuffer.Span)
                         {
-                            return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length);
+                            return new string(charsPtr, 0, _textRun.Length);
                         }
                     }
                 }

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

@@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<DrawableTextRun>? Collapse(TextLine textLine)
+        public override List<TextRun>? Collapse(TextLine textLine)
         {
             return TextEllipsisHelper.Collapse(textLine, this, false);
         }

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

@@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<DrawableTextRun>? Collapse(TextLine textLine)
+        public override List<TextRun>? Collapse(TextLine textLine)
         {
             return TextEllipsisHelper.Collapse(textLine, this, true);
         }

+ 11 - 2
src/Avalonia.Base/Media/TranslateTransform.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media
@@ -25,8 +26,6 @@ namespace Avalonia.Media
         /// </summary>
         public TranslateTransform()
         {
-            this.GetObservable(XProperty).Subscribe(_ => RaiseChanged());
-            this.GetObservable(YProperty).Subscribe(_ => RaiseChanged());
         }
 
         /// <summary>
@@ -63,5 +62,15 @@ namespace Avalonia.Media
         /// Gets the transform's <see cref="Matrix"/>.
         /// </summary>
         public override Matrix Value => Matrix.CreateTranslation(X, Y);
+        
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == XProperty || change.Property == YProperty)
+            {
+                RaiseChanged();
+            }
+        }
     }
 }

+ 1 - 1
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Data;
 using Avalonia.Threading;
 

+ 62 - 0
src/Avalonia.Base/Reactive/AnonymousObserver.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Avalonia.Reactive;
+
+internal class AnonymousObserver<T> : IObserver<T>
+{
+    private static readonly Action<Exception> ThrowsOnError = ex => throw ex;
+    private static readonly Action NoOpCompleted = () => { };  
+    private readonly Action<T> _onNext;
+    private readonly Action<Exception> _onError;
+    private readonly Action _onCompleted;
+
+    public AnonymousObserver(TaskCompletionSource<T> tcs)
+    {
+        if (tcs is null)
+        {
+            throw new ArgumentNullException(nameof(tcs));
+        }
+
+        _onNext = tcs.SetResult;
+        _onError = tcs.SetException;
+        _onCompleted = NoOpCompleted;
+    }
+    
+    public AnonymousObserver(Action<T> onNext, Action<Exception> onError, Action onCompleted)
+    {
+        _onNext = onNext ?? throw new ArgumentNullException(nameof(onNext));
+        _onError = onError ?? throw new ArgumentNullException(nameof(onError));
+        _onCompleted = onCompleted ?? throw new ArgumentNullException(nameof(onCompleted));
+    }
+
+    public AnonymousObserver(Action<T> onNext)
+        : this(onNext, ThrowsOnError, NoOpCompleted)
+    {
+    }
+
+    public AnonymousObserver(Action<T> onNext, Action<Exception> onError)
+        : this(onNext, onError, NoOpCompleted)
+    {
+    }
+
+    public AnonymousObserver(Action<T> onNext, Action onCompleted)
+        : this(onNext, ThrowsOnError, onCompleted)
+    {
+    }
+
+    public void OnCompleted()
+    {
+        _onCompleted.Invoke();
+    }
+
+    public void OnError(Exception error)
+    {
+        _onError.Invoke(error);
+    }
+
+    public void OnNext(T value)
+    {
+        _onNext.Invoke(value);
+    }
+}

+ 23 - 0
src/Avalonia.Base/Reactive/CombinedSubject.cs

@@ -0,0 +1,23 @@
+using System;
+
+namespace Avalonia.Reactive;
+
+internal class CombinedSubject<T> : IAvaloniaSubject<T>
+{
+    private readonly IObserver<T> _observer;
+    private readonly IObservable<T> _observable;
+
+    public CombinedSubject(IObserver<T> observer, IObservable<T> observable)
+    {
+        _observer = observer;
+        _observable = observable;
+    }
+
+    public void OnCompleted() => _observer.OnCompleted();
+
+    public void OnError(Exception error) => _observer.OnError(error);
+
+    public void OnNext(T value) => _observer.OnNext(value);
+
+    public IDisposable Subscribe(IObserver<T> observer) => _observable.Subscribe(observer);
+}

+ 427 - 0
src/Avalonia.Base/Reactive/CompositeDisposable.cs

@@ -0,0 +1,427 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Avalonia.Reactive;
+
+internal sealed class CompositeDisposable : ICollection<IDisposable>, IDisposable
+{
+    private readonly object _gate = new object();
+    private bool _disposed;
+    private List<IDisposable?> _disposables;
+    private int _count;
+    private const int ShrinkThreshold = 64;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CompositeDisposable"/> class with the specified number of disposables.
+    /// </summary>
+    /// <param name="capacity">The number of disposables that the new CompositeDisposable can initially store.</param>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
+    public CompositeDisposable(int capacity)
+    {
+        if (capacity < 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(capacity));
+        }
+
+        _disposables = new List<IDisposable?>(capacity);
+    }
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CompositeDisposable"/> class from a group of disposables.
+    /// </summary>
+    /// <param name="disposables">Disposables that will be disposed together.</param>
+    /// <exception cref="ArgumentNullException"><paramref name="disposables"/> is <c>null</c>.</exception>
+    /// <exception cref="ArgumentException">Any of the disposables in the <paramref name="disposables"/> collection is <c>null</c>.</exception>
+    public CompositeDisposable(params IDisposable[] disposables)
+    {
+        if (disposables == null)
+        {
+            throw new ArgumentNullException(nameof(disposables));
+        }
+
+        _disposables = ToList(disposables);
+
+        // _count can be read by other threads and thus should be properly visible
+        // also releases the _disposables contents so it becomes thread-safe
+        Volatile.Write(ref _count, _disposables.Count);
+    }
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CompositeDisposable"/> class from a group of disposables.
+    /// </summary>
+    /// <param name="disposables">Disposables that will be disposed together.</param>
+    /// <exception cref="ArgumentNullException"><paramref name="disposables"/> is <c>null</c>.</exception>
+    /// <exception cref="ArgumentException">Any of the disposables in the <paramref name="disposables"/> collection is <c>null</c>.</exception>
+    public CompositeDisposable(IList<IDisposable> disposables)
+    {
+        if (disposables == null)
+        {
+            throw new ArgumentNullException(nameof(disposables));
+        }
+
+        _disposables = ToList(disposables);
+
+        // _count can be read by other threads and thus should be properly visible
+        // also releases the _disposables contents so it becomes thread-safe
+        Volatile.Write(ref _count, _disposables.Count);
+    }
+
+    private static List<IDisposable?> ToList(IEnumerable<IDisposable> disposables)
+    {
+        var capacity = disposables switch
+        {
+            IDisposable[] a => a.Length,
+            ICollection<IDisposable> c => c.Count,
+            _ => 12
+        };
+
+        var list = new List<IDisposable?>(capacity);
+
+        // do the copy and null-check in one step to avoid a
+        // second loop for just checking for null items
+        foreach (var d in disposables)
+        {
+            if (d == null)
+            {
+                throw new ArgumentException("Disposables can't contain null", nameof(disposables));
+            }
+
+            list.Add(d);
+        }
+
+        return list;
+    }
+
+    /// <summary>
+    /// Gets the number of disposables contained in the <see cref="CompositeDisposable"/>.
+    /// </summary>
+    public int Count => Volatile.Read(ref _count);
+
+    /// <summary>
+    /// Adds a disposable to the <see cref="CompositeDisposable"/> or disposes the disposable if the <see cref="CompositeDisposable"/> is disposed.
+    /// </summary>
+    /// <param name="item">Disposable to add.</param>
+    /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
+    public void Add(IDisposable item)
+    {
+        if (item == null)
+        {
+            throw new ArgumentNullException(nameof(item));
+        }
+
+        lock (_gate)
+        {
+            if (!_disposed)
+            {
+                _disposables.Add(item);
+
+                // If read atomically outside the lock, it should be written atomically inside
+                // the plain read on _count is fine here because manipulation always happens
+                // from inside a lock.
+                Volatile.Write(ref _count, _count + 1);
+                return;
+            }
+        }
+
+        item.Dispose();
+    }
+
+    /// <summary>
+    /// Removes and disposes the first occurrence of a disposable from the <see cref="CompositeDisposable"/>.
+    /// </summary>
+    /// <param name="item">Disposable to remove.</param>
+    /// <returns>true if found; false otherwise.</returns>
+    /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
+    public bool Remove(IDisposable item)
+    {
+        if (item == null)
+        {
+            throw new ArgumentNullException(nameof(item));
+        }
+
+        lock (_gate)
+        {
+            // this composite was already disposed and if the item was in there
+            // it has been already removed/disposed
+            if (_disposed)
+            {
+                return false;
+            }
+
+            //
+            // List<T> doesn't shrink the size of the underlying array but does collapse the array
+            // by copying the tail one position to the left of the removal index. We don't need
+            // index-based lookup but only ordering for sequential disposal. So, instead of spending
+            // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also
+            // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it.
+            //
+
+            // read fields as infrequently as possible
+            var current = _disposables;
+
+            var i = current.IndexOf(item);
+            if (i < 0)
+            {
+                // not found, just return
+                return false;
+            }
+
+            current[i] = null;
+
+            if (current.Capacity > ShrinkThreshold && _count < current.Capacity / 2)
+            {
+                var fresh = new List<IDisposable?>(current.Capacity / 2);
+
+                foreach (var d in current)
+                {
+                    if (d != null)
+                    {
+                        fresh.Add(d);
+                    }
+                }
+
+                _disposables = fresh;
+            }
+
+            // make sure the Count property sees an atomic update
+            Volatile.Write(ref _count, _count - 1);
+        }
+
+        // if we get here, the item was found and removed from the list
+        // just dispose it and report success
+
+        item.Dispose();
+
+        return true;
+    }
+
+    /// <summary>
+    /// Disposes all disposables in the group and removes them from the group.
+    /// </summary>
+    public void Dispose()
+    {
+        List<IDisposable?>? currentDisposables = null;
+
+        lock (_gate)
+        {
+            if (!_disposed)
+            {
+                currentDisposables = _disposables;
+
+                // nulling out the reference is faster no risk to
+                // future Add/Remove because _disposed will be true
+                // and thus _disposables won't be touched again.
+                _disposables = null!; // NB: All accesses are guarded by _disposed checks.
+
+                Volatile.Write(ref _count, 0);
+                Volatile.Write(ref _disposed, true);
+            }
+        }
+
+        if (currentDisposables != null)
+        {
+            foreach (var d in currentDisposables)
+            {
+                d?.Dispose();
+            }
+        }
+    }
+
+    /// <summary>
+    /// Removes and disposes all disposables from the <see cref="CompositeDisposable"/>, but does not dispose the <see cref="CompositeDisposable"/>.
+    /// </summary>
+    public void Clear()
+    {
+        IDisposable?[] previousDisposables;
+
+        lock (_gate)
+        {
+            // disposed composites are always clear
+            if (_disposed)
+            {
+                return;
+            }
+
+            var current = _disposables;
+
+            previousDisposables = current.ToArray();
+            current.Clear();
+
+            Volatile.Write(ref _count, 0);
+        }
+
+        foreach (var d in previousDisposables)
+        {
+            d?.Dispose();
+        }
+    }
+
+    /// <summary>
+    /// Determines whether the <see cref="CompositeDisposable"/> contains a specific disposable.
+    /// </summary>
+    /// <param name="item">Disposable to search for.</param>
+    /// <returns>true if the disposable was found; otherwise, false.</returns>
+    /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
+    public bool Contains(IDisposable item)
+    {
+        if (item == null)
+        {
+            throw new ArgumentNullException(nameof(item));
+        }
+
+        lock (_gate)
+        {
+            if (_disposed)
+            {
+                return false;
+            }
+
+            return _disposables.Contains(item);
+        }
+    }
+
+    /// <summary>
+    /// Copies the disposables contained in the <see cref="CompositeDisposable"/> to an array, starting at a particular array index.
+    /// </summary>
+    /// <param name="array">Array to copy the contained disposables to.</param>
+    /// <param name="arrayIndex">Target index at which to copy the first disposable of the group.</param>
+    /// <exception cref="ArgumentNullException"><paramref name="array"/> is <c>null</c>.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="arrayIndex"/> is less than zero. -or - <paramref name="arrayIndex"/> is larger than or equal to the array length.</exception>
+    public void CopyTo(IDisposable[] array, int arrayIndex)
+    {
+        if (array == null)
+        {
+            throw new ArgumentNullException(nameof(array));
+        }
+
+        if (arrayIndex < 0 || arrayIndex >= array.Length)
+        {
+            throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+        }
+
+        lock (_gate)
+        {
+            // disposed composites are always empty
+            if (_disposed)
+            {
+                return;
+            }
+
+            if (arrayIndex + _count > array.Length)
+            {
+                // there is not enough space beyond arrayIndex 
+                // to accommodate all _count disposables in this composite
+                throw new ArgumentOutOfRangeException(nameof(arrayIndex));
+            }
+
+            var i = arrayIndex;
+
+            foreach (var d in _disposables)
+            {
+                if (d != null)
+                {
+                    array[i++] = d;
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    /// Always returns false.
+    /// </summary>
+    public bool IsReadOnly => false;
+
+    /// <summary>
+    /// Returns an enumerator that iterates through the <see cref="CompositeDisposable"/>.
+    /// </summary>
+    /// <returns>An enumerator to iterate over the disposables.</returns>
+    public IEnumerator<IDisposable> GetEnumerator()
+    {
+        lock (_gate)
+        {
+            if (_disposed || _count == 0)
+            {
+                return EmptyEnumerator;
+            }
+
+            // the copy is unavoidable but the creation
+            // of an outer IEnumerable is avoidable
+            return new CompositeEnumerator(_disposables.ToArray());
+        }
+    }
+
+    /// <summary>
+    /// Returns an enumerator that iterates through the <see cref="CompositeDisposable"/>.
+    /// </summary>
+    /// <returns>An enumerator to iterate over the disposables.</returns>
+    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+    /// <summary>
+    /// Gets a value that indicates whether the object is disposed.
+    /// </summary>
+    public bool IsDisposed => Volatile.Read(ref _disposed);
+
+    /// <summary>
+    /// An empty enumerator for the <see cref="GetEnumerator"/>
+    /// method to avoid allocation on disposed or empty composites.
+    /// </summary>
+    private static readonly CompositeEnumerator EmptyEnumerator =
+        new CompositeEnumerator(Array.Empty<IDisposable?>());
+
+    /// <summary>
+    /// An enumerator for an array of disposables.
+    /// </summary>
+    private sealed class CompositeEnumerator : IEnumerator<IDisposable>
+    {
+        private readonly IDisposable?[] _disposables;
+        private int _index;
+
+        public CompositeEnumerator(IDisposable?[] disposables)
+        {
+            _disposables = disposables;
+            _index = -1;
+        }
+
+        public IDisposable Current => _disposables[_index]!; // NB: _index is only advanced to non-null positions.
+
+        object IEnumerator.Current => _disposables[_index]!;
+
+        public void Dispose()
+        {
+            // Avoid retention of the referenced disposables
+            // beyond the lifecycle of the enumerator.
+            // Not sure if this happens by default to
+            // generic array enumerators though.
+            var disposables = _disposables;
+            Array.Clear(disposables, 0, disposables.Length);
+        }
+
+        public bool MoveNext()
+        {
+            var disposables = _disposables;
+
+            for (;;)
+            {
+                var idx = ++_index;
+
+                if (idx >= disposables.Length)
+                {
+                    return false;
+                }
+
+                // inlined that filter for null elements
+                if (disposables[idx] != null)
+                {
+                    return true;
+                }
+            }
+        }
+
+        public void Reset()
+        {
+            _index = -1;
+        }
+    }
+}

+ 98 - 0
src/Avalonia.Base/Reactive/Disposable.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Threading;
+
+namespace Avalonia.Reactive;
+
+/// <summary>
+/// Provides a set of static methods for creating <see cref="IDisposable"/> objects.
+/// </summary>
+internal static class Disposable
+{
+    /// <summary>
+    /// Represents a disposable that does nothing on disposal.
+    /// </summary>
+    private sealed class EmptyDisposable : IDisposable
+    {
+        public static readonly EmptyDisposable Instance = new();
+
+        private EmptyDisposable()
+        {
+        }
+
+        public void Dispose()
+        {
+            // no op
+        }
+    }
+    
+    internal sealed class AnonymousDisposable : IDisposable
+    {
+        private volatile Action? _dispose;
+        public AnonymousDisposable(Action dispose)
+        {
+            _dispose = dispose;
+        }
+        public bool IsDisposed => _dispose == null;
+        public void Dispose()
+        {
+            Interlocked.Exchange(ref _dispose, null)?.Invoke();
+        }
+    }
+
+    internal sealed class AnonymousDisposable<TState> : IDisposable
+    {
+        private TState _state;
+        private volatile Action<TState>? _dispose;
+
+        public AnonymousDisposable(TState state, Action<TState> dispose)
+        {
+            _state = state;
+            _dispose = dispose;
+        }
+
+        public bool IsDisposed => _dispose == null;
+        public void Dispose()
+        {
+            Interlocked.Exchange(ref _dispose, null)?.Invoke(_state);
+            _state = default!;
+        }
+    }
+
+    /// <summary>
+    /// Gets the disposable that does nothing when disposed.
+    /// </summary>
+    public static IDisposable Empty => EmptyDisposable.Instance;
+
+    /// <summary>
+    /// Creates a disposable object that invokes the specified action when disposed.
+    /// </summary>
+    /// <param name="dispose">Action to run during the first call to <see cref="IDisposable.Dispose"/>. The action is guaranteed to be run at most once.</param>
+    /// <returns>The disposable object that runs the given action upon disposal.</returns>
+    /// <exception cref="ArgumentNullException"><paramref name="dispose"/> is <c>null</c>.</exception>
+    public static IDisposable Create(Action dispose)
+    {
+        if (dispose == null)
+        {
+            throw new ArgumentNullException(nameof(dispose));
+        }
+
+        return new AnonymousDisposable(dispose);
+    }
+
+    /// <summary>
+    /// Creates a disposable object that invokes the specified action when disposed.
+    /// </summary>
+    /// <param name="state">The state to be passed to the action.</param>
+    /// <param name="dispose">Action to run during the first call to <see cref="IDisposable.Dispose"/>. The action is guaranteed to be run at most once.</param>
+    /// <returns>The disposable object that runs the given action upon disposal.</returns>
+    /// <exception cref="ArgumentNullException"><paramref name="dispose"/> is <c>null</c>.</exception>
+    public static IDisposable Create<TState>(TState state, Action<TState> dispose)
+    {
+        if (dispose == null)
+        {
+            throw new ArgumentNullException(nameof(dispose));
+        }
+
+        return new AnonymousDisposable<TState>(state, dispose);
+    }
+}

+ 37 - 0
src/Avalonia.Base/Reactive/DisposableMixin.cs

@@ -0,0 +1,37 @@
+using System;
+using Avalonia.Reactive;
+
+namespace Avalonia.Reactive;
+
+/// <summary>
+/// Extension methods associated with the IDisposable interface.
+/// </summary>
+internal static class DisposableMixin
+{
+    /// <summary>
+    /// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
+    /// </summary>
+    /// <typeparam name="T">
+    /// The type of the disposable.
+    /// </typeparam>
+    /// <param name="item">
+    /// The disposable we are going to want to be disposed by the CompositeDisposable.
+    /// </param>
+    /// <param name="compositeDisposable">
+    /// The <see cref="CompositeDisposable"/> to which <paramref name="item"/> will be added.
+    /// </param>
+    /// <returns>
+    /// The disposable.
+    /// </returns>
+    public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
+        where T : IDisposable
+    {
+        if (compositeDisposable is null)
+        {
+            throw new ArgumentNullException(nameof(compositeDisposable));
+        }
+
+        compositeDisposable.Add(item);
+        return item;
+    }
+}

+ 8 - 0
src/Avalonia.Base/Reactive/IAvaloniaSubject.cs

@@ -0,0 +1,8 @@
+using System;
+
+namespace Avalonia.Reactive;
+
+internal interface IAvaloniaSubject<T> : IObserver<T>, IObservable<T> /*, ISubject<T> */
+{
+    
+}

+ 4 - 4
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@@ -1,7 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive;
-using System.Reactive.Disposables;
 using System.Threading;
 using Avalonia.Threading;
 
@@ -12,7 +10,7 @@ namespace Avalonia.Reactive
     /// </summary>
     /// <typeparam name="T">The observable type.</typeparam>
     /// <remarks>
-    /// <see cref="ObservableBase{T}"/> is rather heavyweight in terms of allocations and memory
+    /// ObservableBase{T} is rather heavyweight in terms of allocations and memory
     /// usage. This class provides a more lightweight base for some internal observable types
     /// in the Avalonia framework.
     /// </remarks>
@@ -21,11 +19,13 @@ namespace Avalonia.Reactive
         private Exception? _error;
         private List<IObserver<T>>? _observers = new List<IObserver<T>>();
 
+        public bool HasObservers => _observers?.Count > 0;
+        
         public IDisposable Subscribe(IObserver<T> observer)
         {
             _ = observer ?? throw new ArgumentNullException(nameof(observer));
 
-            Dispatcher.UIThread.VerifyAccess();
+            //Dispatcher.UIThread.VerifyAccess();
 
             var first = false;
 

+ 30 - 0
src/Avalonia.Base/Reactive/LightweightSubject.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Avalonia.Threading;
+
+namespace Avalonia.Reactive;
+
+internal class LightweightSubject<T> : LightweightObservableBase<T>, IAvaloniaSubject<T>
+{
+    public void OnCompleted()
+    {
+        PublishCompleted();
+    }
+
+    public void OnError(Exception error)
+    {
+        PublishError(error);
+    }
+
+    public void OnNext(T value)
+    {
+        PublishNext(value);
+    }
+
+    protected override void Initialize() { }
+
+    protected override void Deinitialize() { }
+}

+ 247 - 0
src/Avalonia.Base/Reactive/Observable.cs

@@ -0,0 +1,247 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Reactive.Operators;
+using Avalonia.Threading;
+
+namespace Avalonia.Reactive;
+
+/// <summary>
+/// Provides common observable methods as a replacement for the Rx framework.
+/// </summary>
+internal static class Observable
+{
+    public static IObservable<TSource> Create<TSource>(Func<IObserver<TSource>, IDisposable> subscribe)
+    {
+        return new CreateWithDisposableObservable<TSource>(subscribe);
+    }
+
+    public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> action)
+    {
+        return source.Subscribe(new AnonymousObserver<T>(action));
+    }
+
+    public static IObservable<TResult> Select<TSource, TResult>(this IObservable<TSource> source, Func<TSource, TResult> selector)
+    {
+        return Create<TResult>(obs =>
+        {
+            return source.Subscribe(new AnonymousObserver<TSource>(
+                input =>
+                {
+                    TResult value;
+                    try
+                    {
+                        value = selector(input);
+                    }
+                    catch (Exception ex)
+                    {
+                        obs.OnError(ex);
+                        return;
+                    }
+
+                    obs.OnNext(value);
+                }, obs.OnError, obs.OnCompleted));
+        });
+    }
+
+    public static IObservable<TSource> StartWith<TSource>(this IObservable<TSource> source, TSource value)
+    {
+        return Create<TSource>(obs =>
+        {
+            obs.OnNext(value);
+            return source.Subscribe(obs);
+        });
+    }
+    
+    public static IObservable<TSource> Where<TSource>(this IObservable<TSource> source, Func<TSource, bool> predicate)
+    {
+        return Create<TSource>(obs =>
+        {
+            return source.Subscribe(new AnonymousObserver<TSource>(
+                input =>
+                {
+                    bool shouldRun;
+                    try
+                    {
+                        shouldRun = predicate(input);
+                    }
+                    catch (Exception ex)
+                    {
+                        obs.OnError(ex);
+                        return;
+                    }
+                    if (shouldRun)
+                    {
+                        obs.OnNext(input);
+                    }
+                }, obs.OnError, obs.OnCompleted));
+        });
+    }
+
+    public static IObservable<TSource> Switch<TSource>(
+        this IObservable<IObservable<TSource>> sources)
+    {
+        return new Switch<TSource>(sources);
+    }
+
+    public static IObservable<TResult> CombineLatest<TFirst, TSecond, TResult>(
+        this IObservable<TFirst> first, IObservable<TSecond> second,
+        Func<TFirst, TSecond, TResult> resultSelector)
+    {
+        return new CombineLatest<TFirst, TSecond, TResult>(first, second, resultSelector);
+    }
+    
+    public static IObservable<TInput[]> CombineLatest<TInput>(
+        this IEnumerable<IObservable<TInput>> inputs)
+    {
+        return new CombineLatest<TInput, TInput[]>(inputs, items => items);
+    }
+
+    public static IObservable<T> Skip<T>(this IObservable<T> source, int skipCount)
+    {
+        if (skipCount <= 0)
+        {
+            throw new ArgumentException("Skip count must be bigger than zero", nameof(skipCount));
+        }
+
+        return Create<T>(obs =>
+        {
+            var remaining = skipCount;
+            return source.Subscribe(new AnonymousObserver<T>(
+                input =>
+                {
+                    if (remaining <= 0)
+                    {
+                        obs.OnNext(input);
+                    }
+                    else
+                    {
+                        remaining--;
+                    }
+                }, obs.OnError, obs.OnCompleted));
+        });
+    }
+    
+    public static IObservable<T> Take<T>(this IObservable<T> source, int takeCount)
+    {
+        if (takeCount <= 0)
+        {
+            return Empty<T>();
+        }
+
+        return Create<T>(obs =>
+        {
+            var remaining = takeCount;
+            IDisposable? sub = null;
+            sub = source.Subscribe(new AnonymousObserver<T>(
+                input =>
+                {
+                    if (remaining > 0)
+                    {
+                        --remaining;
+                        obs.OnNext(input);
+
+                        if (remaining == 0)
+                        {
+                            sub?.Dispose();
+                            obs.OnCompleted();
+                        }
+                    }
+                }, obs.OnError, obs.OnCompleted));
+            return sub;
+        });
+    }
+
+    public static IObservable<EventArgs> FromEventPattern(Action<EventHandler> addHandler, Action<EventHandler> removeHandler)
+    {
+        return Create<EventArgs>(observer =>
+        {
+            var handler = new Action<EventArgs>(observer.OnNext);
+            var converted = new EventHandler((_, args) => handler(args));
+            addHandler(converted);
+
+            return Disposable.Create(() => removeHandler(converted));
+        });
+    }
+    
+    public static IObservable<T> Return<T>(T value)
+    {
+        return new ReturnImpl<T>(value);
+    }
+    
+    public static IObservable<T> Empty<T>()
+    {
+        return EmptyImpl<T>.Instance;
+    }
+        
+    /// <summary>
+    /// Returns an observable that fires once with the specified value and never completes.
+    /// </summary>
+    /// <typeparam name="T">The type of the value.</typeparam>
+    /// <param name="value">The value.</param>
+    /// <returns>The observable.</returns>
+    public static IObservable<T> SingleValue<T>(T value)
+    {
+        return new SingleValueImpl<T>(value);
+    }
+ 
+    private sealed class SingleValueImpl<T> : IObservable<T>
+    {
+        private readonly T _value;
+
+        public SingleValueImpl(T value)
+        {
+            _value = value;
+        }
+        public IDisposable Subscribe(IObserver<T> observer)
+        {
+            observer.OnNext(_value);
+            return Disposable.Empty;
+        }
+    }
+    
+    private sealed class ReturnImpl<T> : IObservable<T>
+    {
+        private readonly T _value;
+
+        public ReturnImpl(T value)
+        {
+            _value = value;
+        }
+        public IDisposable Subscribe(IObserver<T> observer)
+        {
+            observer.OnNext(_value);
+            observer.OnCompleted();
+            return Disposable.Empty;
+        }
+    }
+    
+    internal sealed class EmptyImpl<TResult> : IObservable<TResult>
+    {
+        internal static readonly IObservable<TResult> Instance = new EmptyImpl<TResult>();
+
+        private EmptyImpl() { }
+
+        public IDisposable Subscribe(IObserver<TResult> observer)
+        {
+            observer.OnCompleted();
+            return Disposable.Empty;
+        }
+    }
+    
+    private sealed class CreateWithDisposableObservable<TSource> : IObservable<TSource>
+    {
+        private readonly Func<IObserver<TSource>, IDisposable> _subscribe;
+
+        public CreateWithDisposableObservable(Func<IObserver<TSource>, IDisposable> subscribe)
+        {
+            _subscribe = subscribe;
+        }
+
+        public IDisposable Subscribe(IObserver<TSource> observer)
+        {
+            return _subscribe(observer);
+        }
+    }
+}

+ 0 - 37
src/Avalonia.Base/Reactive/ObservableEx.cs

@@ -1,37 +0,0 @@
-using System;
-using System.Reactive.Disposables;
-
-namespace Avalonia.Reactive
-{
-    /// <summary>
-    /// Provides common observable methods not found in standard Rx framework.
-    /// </summary>
-    public static class ObservableEx
-    {
-        /// <summary>
-        /// Returns an observable that fires once with the specified value and never completes.
-        /// </summary>
-        /// <typeparam name="T">The type of the value.</typeparam>
-        /// <param name="value">The value.</param>
-        /// <returns>The observable.</returns>
-        public static IObservable<T> SingleValue<T>(T value)
-        {
-            return new SingleValueImpl<T>(value);
-        }
- 
-        private class SingleValueImpl<T> : IObservable<T>
-        {
-            private T _value;
-
-            public SingleValueImpl(T value)
-            {
-                _value = value;
-            }
-            public IDisposable Subscribe(IObserver<T> observer)
-            {
-                observer.OnNext(_value);
-                return Disposable.Empty;
-            }
-        }
-    }
-}

+ 374 - 0
src/Avalonia.Base/Reactive/Operators/CombineLatest.cs

@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Avalonia.Reactive.Operators;
+
+// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/CombineLatest.cs
+
+internal sealed class CombineLatest<TFirst, TSecond, TResult> : IObservable<TResult>
+{
+    private readonly IObservable<TFirst> _first;
+    private readonly IObservable<TSecond> _second;
+    private readonly Func<TFirst, TSecond, TResult> _resultSelector;
+
+    public CombineLatest(IObservable<TFirst> first, IObservable<TSecond> second,
+        Func<TFirst, TSecond, TResult> resultSelector)
+    {
+        _first = first;
+        _second = second;
+        _resultSelector = resultSelector;
+    }
+
+    public IDisposable Subscribe(IObserver<TResult> observer)
+    {
+        var sink = new _(_resultSelector, observer);
+        sink.Run(_first, _second);
+        return sink;
+    }
+
+    internal sealed class _ : IdentitySink<TResult>
+    {
+        private readonly Func<TFirst, TSecond, TResult> _resultSelector;
+        private readonly object _gate = new object();
+
+        public _(Func<TFirst, TSecond, TResult> resultSelector, IObserver<TResult> observer)
+            : base(observer)
+        {
+            _resultSelector = resultSelector;
+            _firstDisposable = null!;
+            _secondDisposable = null!;
+        }
+
+        private IDisposable _firstDisposable;
+        private IDisposable _secondDisposable;
+
+        public void Run(IObservable<TFirst> first, IObservable<TSecond> second)
+        {
+            var fstO = new FirstObserver(this);
+            var sndO = new SecondObserver(this);
+
+            fstO.SetOther(sndO);
+            sndO.SetOther(fstO);
+
+            _firstDisposable = first.Subscribe(fstO);
+            _secondDisposable = second.Subscribe(sndO);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _firstDisposable.Dispose();
+                _secondDisposable.Dispose();
+            }
+
+            base.Dispose(disposing);
+        }
+
+        private sealed class FirstObserver : IObserver<TFirst>
+        {
+            private readonly _ _parent;
+            private SecondObserver _other;
+
+            public FirstObserver(_ parent)
+            {
+                _parent = parent;
+                _other = default!; // NB: Will be set by SetOther.
+            }
+
+            public void SetOther(SecondObserver other) { _other = other; }
+
+            public bool HasValue { get; private set; }
+            public TFirst? Value { get; private set; }
+            public bool Done { get; private set; }
+
+            public void OnNext(TFirst value)
+            {
+                lock (_parent._gate)
+                {
+                    HasValue = true;
+                    Value = value;
+
+                    if (_other.HasValue)
+                    {
+                        TResult res;
+                        try
+                        {
+                            res = _parent._resultSelector(value, _other.Value!);
+                        }
+                        catch (Exception ex)
+                        {
+                            _parent.ForwardOnError(ex);
+                            return;
+                        }
+
+                        _parent.ForwardOnNext(res);
+                    }
+                    else if (_other.Done)
+                    {
+                        _parent.ForwardOnCompleted();
+                    }
+                }
+            }
+
+            public void OnError(Exception error)
+            {
+                lock (_parent._gate)
+                {
+                    _parent.ForwardOnError(error);
+                }
+            }
+
+            public void OnCompleted()
+            {
+                lock (_parent._gate)
+                {
+                    Done = true;
+
+                    if (_other.Done)
+                    {
+                        _parent.ForwardOnCompleted();
+                    }
+                    else
+                    {
+                        _parent._firstDisposable.Dispose();
+                    }
+                }
+            }
+        }
+
+        private sealed class SecondObserver : IObserver<TSecond>
+        {
+            private readonly _ _parent;
+            private FirstObserver _other;
+
+            public SecondObserver(_ parent)
+            {
+                _parent = parent;
+                _other = default!; // NB: Will be set by SetOther.
+            }
+
+            public void SetOther(FirstObserver other) { _other = other; }
+
+            public bool HasValue { get; private set; }
+            public TSecond? Value { get; private set; }
+            public bool Done { get; private set; }
+
+            public void OnNext(TSecond value)
+            {
+                lock (_parent._gate)
+                {
+                    HasValue = true;
+                    Value = value;
+
+                    if (_other.HasValue)
+                    {
+                        TResult res;
+                        try
+                        {
+                            res = _parent._resultSelector(_other.Value!, value);
+                        }
+                        catch (Exception ex)
+                        {
+                            _parent.ForwardOnError(ex);
+                            return;
+                        }
+
+                        _parent.ForwardOnNext(res);
+                    }
+                    else if (_other.Done)
+                    {
+                        _parent.ForwardOnCompleted();
+                    }
+                }
+            }
+
+            public void OnError(Exception error)
+            {
+                lock (_parent._gate)
+                {
+                    _parent.ForwardOnError(error);
+                }
+            }
+
+            public void OnCompleted()
+            {
+                lock (_parent._gate)
+                {
+                    Done = true;
+
+                    if (_other.Done)
+                    {
+                        _parent.ForwardOnCompleted();
+                    }
+                    else
+                    {
+                        _parent._secondDisposable.Dispose();
+                    }
+                }
+            }
+        }
+    }
+}
+
+internal sealed class CombineLatest<TSource, TResult> : IObservable<TResult>
+{
+    private readonly IEnumerable<IObservable<TSource>> _sources;
+    private readonly Func<TSource[], TResult> _resultSelector;
+
+    public CombineLatest(IEnumerable<IObservable<TSource>> sources, Func<TSource[], TResult> resultSelector)
+    {
+        _sources = sources;
+        _resultSelector = resultSelector;
+    }
+
+    public IDisposable Subscribe(IObserver<TResult> observer)
+    {
+        var sink = new _(_resultSelector, observer);
+        sink.Run(_sources);
+        return sink;
+    }
+
+    internal sealed class _ : IdentitySink<TResult>
+    {
+        private readonly object _gate = new object();
+        private readonly Func<TSource[], TResult> _resultSelector;
+
+        public _(Func<TSource[], TResult> resultSelector, IObserver<TResult> observer)
+            : base(observer)
+        {
+            _resultSelector = resultSelector;
+
+            // NB: These will be set in Run before getting used.
+            _hasValue = null!;
+            _values = null!;
+            _isDone = null!;
+            _subscriptions = null!;
+        }
+
+        private bool[] _hasValue;
+        private bool _hasValueAll;
+        private TSource[] _values;
+        private bool[] _isDone;
+        private IDisposable[] _subscriptions;
+
+        public void Run(IEnumerable<IObservable<TSource>> sources)
+        {
+            var srcs = sources.ToArray();
+
+            var N = srcs.Length;
+
+            _hasValue = new bool[N];
+            _hasValueAll = false;
+
+            _values = new TSource[N];
+
+            _isDone = new bool[N];
+
+            _subscriptions = new IDisposable[N];
+
+            for (var i = 0; i < N; i++)
+            {
+                var j = i;
+
+                var o = new SourceObserver(this, j);
+                _subscriptions[j] = o;
+
+                o.Disposable = srcs[j].Subscribe(o);
+            }
+
+            SetUpstream(new CompositeDisposable(_subscriptions));
+        }
+
+        private void OnNext(int index, TSource value)
+        {
+            lock (_gate)
+            {
+                _values[index] = value;
+
+                _hasValue[index] = true;
+
+                if (_hasValueAll || (_hasValueAll = _hasValue.All(v => v)))
+                {
+                    TResult res;
+                    try
+                    {
+                        res = _resultSelector(_values);
+                    }
+                    catch (Exception ex)
+                    {
+                        ForwardOnError(ex);
+                        return;
+                    }
+
+                    ForwardOnNext(res);
+                }
+                else if (_isDone.Where((_, i) => i != index).All(d => d))
+                {
+                    ForwardOnCompleted();
+                }
+            }
+        }
+
+        private new void OnError(Exception error)
+        {
+            lock (_gate)
+            {
+                ForwardOnError(error);
+            }
+        }
+
+        private void OnCompleted(int index)
+        {
+            lock (_gate)
+            {
+                _isDone[index] = true;
+
+                if (_isDone.All(d => d))
+                {
+                    ForwardOnCompleted();
+                }
+                else
+                {
+                    _subscriptions[index].Dispose();
+                }
+            }
+        }
+
+        private sealed class SourceObserver : IObserver<TSource>, IDisposable
+        {
+            private readonly _ _parent;
+            private readonly int _index;
+
+            public SourceObserver(_ parent, int index)
+            {
+                _parent = parent;
+                _index = index;
+            }
+
+            public IDisposable? Disposable { get; set; }
+
+            public void OnNext(TSource value)
+            {
+                _parent.OnNext(_index, value);
+            }
+
+            public void OnError(Exception error)
+            {
+                _parent.OnError(error);
+            }
+
+            public void OnCompleted()
+            {
+                _parent.OnCompleted(_index);
+            }
+
+            public void Dispose()
+            {
+                Disposable?.Dispose();
+            }
+        }
+    }
+}

+ 111 - 0
src/Avalonia.Base/Reactive/Operators/Sink.cs

@@ -0,0 +1,111 @@
+using System;
+using System.Threading;
+
+namespace Avalonia.Reactive.Operators;
+
+// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Internal/Sink.cs
+
+internal abstract class Sink<TTarget> : IDisposable
+{
+    private IDisposable? _upstream;
+    private volatile IObserver<TTarget> _observer;
+
+    protected Sink(IObserver<TTarget> observer)
+    {
+        _observer = observer;
+    }
+
+    public void Dispose()
+    {
+        Dispose(true);
+    }
+
+    /// <summary>
+    /// Override this method to dispose additional resources.
+    /// The method is guaranteed to be called at most once.
+    /// </summary>
+    /// <param name="disposing">If true, the method was called from <see cref="Dispose()"/>.</param>
+    protected virtual void Dispose(bool disposing)
+    {
+        //Calling base.Dispose(true) is not a proper disposal, so we can omit the assignment here.
+        //Sink is internal so this can pretty much be enforced.
+        //_observer = NopObserver<TTarget>.Instance;
+
+        _upstream?.Dispose();
+    }
+
+    public void ForwardOnNext(TTarget value)
+    {
+        _observer.OnNext(value);
+    }
+
+    public void ForwardOnCompleted()
+    {
+        _observer.OnCompleted();
+        Dispose();
+    }
+
+    public void ForwardOnError(Exception error)
+    {
+        _observer.OnError(error);
+        Dispose();
+    }
+
+    protected void SetUpstream(IDisposable upstream)
+    {
+        _upstream = upstream;
+    }
+
+    protected void DisposeUpstream()
+    {
+        _upstream?.Dispose();
+    }
+}
+
+internal abstract class Sink<TSource, TTarget> : Sink<TTarget>, IObserver<TSource>
+{
+    protected Sink(IObserver<TTarget> observer) : base(observer)
+    {
+    }
+
+    public virtual void Run(IObservable<TSource> source)
+    {
+        SetUpstream(source.Subscribe(this));
+    }
+
+    public abstract void OnNext(TSource value);
+
+    public virtual void OnError(Exception error) => ForwardOnError(error);
+
+    public virtual void OnCompleted() => ForwardOnCompleted();
+
+    public IObserver<TTarget> GetForwarder() => new _(this);
+
+    private sealed class _ : IObserver<TTarget>
+    {
+        private readonly Sink<TSource, TTarget> _forward;
+
+        public _(Sink<TSource, TTarget> forward)
+        {
+            _forward = forward;
+        }
+
+        public void OnNext(TTarget value) => _forward.ForwardOnNext(value);
+
+        public void OnError(Exception error) => _forward.ForwardOnError(error);
+
+        public void OnCompleted() => _forward.ForwardOnCompleted();
+    }
+}
+
+internal abstract class IdentitySink<T> : Sink<T, T>
+{
+    protected IdentitySink(IObserver<T> observer) : base(observer)
+    {
+    }
+
+    public override void OnNext(T value)
+    {
+        ForwardOnNext(value);
+    }
+}

+ 144 - 0
src/Avalonia.Base/Reactive/Operators/Switch.cs

@@ -0,0 +1,144 @@
+using System;
+
+namespace Avalonia.Reactive.Operators;
+
+// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/Switch.cs
+
+internal sealed class Switch<TSource> : IObservable<TSource>
+{
+    private readonly IObservable<IObservable<TSource>> _sources;
+
+    public Switch(IObservable<IObservable<TSource>> sources)
+    {
+        _sources = sources;
+    }
+
+    public IDisposable Subscribe(IObserver<TSource> observer)
+    {
+        return _sources.Subscribe(new _(observer));
+    }
+
+    internal sealed class _ : Sink<IObservable<TSource>, TSource>
+    {
+        private readonly object _gate = new object();
+
+        public _(IObserver<TSource> observer)
+            : base(observer)
+        {
+        }
+
+        private IDisposable? _innerSerialDisposable;
+        private bool _isStopped;
+        private ulong _latest;
+        private bool _hasLatest;
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _innerSerialDisposable?.Dispose();
+            }
+
+            base.Dispose(disposing);
+        }
+
+        public override void OnNext(IObservable<TSource> value)
+        {
+            ulong id;
+
+            lock (_gate)
+            {
+                id = unchecked(++_latest);
+                _hasLatest = true;
+            }
+
+            var innerObserver = new InnerObserver(this, id);
+
+            _innerSerialDisposable = innerObserver;
+            innerObserver.Disposable = value.Subscribe(innerObserver);
+        }
+
+        public override void OnError(Exception error)
+        {
+            lock (_gate)
+            {
+                ForwardOnError(error);
+            }
+        }
+
+        public override void OnCompleted()
+        {
+            lock (_gate)
+            {
+                DisposeUpstream();
+
+                _isStopped = true;
+                if (!_hasLatest)
+                {
+                    ForwardOnCompleted();
+                }
+            }
+        }
+
+        private sealed class InnerObserver : IObserver<TSource>, IDisposable
+        {
+            private readonly _ _parent;
+            private readonly ulong _id;
+
+            public InnerObserver(_ parent, ulong id)
+            {
+                _parent = parent;
+                _id = id;
+            }
+
+            public IDisposable? Disposable { get; set; }
+
+            public void OnNext(TSource value)
+            {
+                lock (_parent._gate)
+                {
+                    if (_parent._latest == _id)
+                    {
+                        _parent.ForwardOnNext(value);
+                    }
+                }
+            }
+
+            public void OnError(Exception error)
+            {
+                lock (_parent._gate)
+                {
+                    Dispose();
+
+                    if (_parent._latest == _id)
+                    {
+                        _parent.ForwardOnError(error);
+                    }
+                }
+            }
+
+            public void OnCompleted()
+            {
+                lock (_parent._gate)
+                {
+                    Dispose();
+
+                    if (_parent._latest == _id)
+                    {
+                        _parent._hasLatest = false;
+
+                        if (_parent._isStopped)
+                        {
+                            _parent.ForwardOnCompleted();
+                        }
+                    }
+                }
+            }
+
+            public void Dispose()
+            {
+                Disposable?.Dispose();
+            }
+        }
+    }
+}

+ 35 - 0
src/Avalonia.Base/Reactive/SerialDisposableValue.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Threading;
+
+namespace Avalonia.Reactive;
+
+/// <summary>
+/// Represents a disposable resource whose underlying disposable resource can be replaced by another disposable resource, causing automatic disposal of the previous underlying disposable resource.
+/// </summary>
+internal sealed class SerialDisposableValue : IDisposable
+{
+    private IDisposable? _current;
+    private bool _disposed;
+    
+    public IDisposable? Disposable
+    {
+        get => _current;
+        set
+        {
+            _current?.Dispose();
+            _current = value;
+            
+            if (_disposed)
+            {
+                _current?.Dispose();
+                _current = null;
+            }
+        }
+    }
+
+    public void Dispose()
+    {
+        _disposed = true;
+        _current?.Dispose();
+    }
+}

+ 1 - 1
src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive.Disposables;
 using Avalonia.Metadata;
 using Avalonia.Platform;
+using Avalonia.Reactive;
 
 namespace Avalonia.Rendering;
 

+ 1 - 1
src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Utilities;

+ 1 - 1
src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Diagnostics;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Threading;
 
 namespace Avalonia.Rendering

+ 3 - 3
src/Avalonia.Base/Styling/StyleInstance.cs

@@ -1,9 +1,9 @@
 using System;
 using System.Collections.Generic;
-using System.Reactive.Subjects;
 using Avalonia.Animation;
 using Avalonia.Data;
 using Avalonia.PropertyStore;
+using Avalonia.Reactive;
 using Avalonia.Styling.Activators;
 
 namespace Avalonia.Styling
@@ -24,7 +24,7 @@ namespace Avalonia.Styling
         private bool _isActive;
         private List<ISetterInstance>? _setters;
         private List<IAnimation>? _animations;
-        private Subject<bool>? _animationTrigger;
+        private LightweightSubject<bool>? _animationTrigger;
 
         public StyleInstance(
             IStyle style,
@@ -67,7 +67,7 @@ namespace Avalonia.Styling
         {
             if (_animations is not null && control is Animatable animatable)
             {
-                _animationTrigger ??= new Subject<bool>();
+                _animationTrigger ??= new LightweightSubject<bool>();
                 foreach (var animation in _animations)
                     animation.Apply(animatable, null, _animationTrigger);
 

+ 1 - 1
src/Avalonia.Base/Threading/DispatcherTimer.cs

@@ -1,5 +1,5 @@
 using System;
-using System.Reactive.Disposables;
+using Avalonia.Reactive;
 using Avalonia.Platform;
 
 namespace Avalonia.Threading

+ 0 - 18
src/Avalonia.Base/Utilities/IWeakSubscriber.cs

@@ -1,18 +0,0 @@
-using System;
-
-namespace Avalonia.Utilities
-{
-    /// <summary>
-    /// Defines a listener to a event subscribed vis the <see cref="WeakObservable"/>.
-    /// </summary>
-    /// <typeparam name="T">The type of the event arguments.</typeparam>
-    public interface IWeakSubscriber<T> where T : EventArgs
-    {
-        /// <summary>
-        /// Invoked when the subscribed event is raised.
-        /// </summary>
-        /// <param name="sender">The event sender.</param>
-        /// <param name="e">The event arguments.</param>
-        void OnEvent(object? sender, T e);
-    }
-}

+ 0 - 60
src/Avalonia.Base/Utilities/WeakObservable.cs

@@ -1,60 +0,0 @@
-using System;
-using System.Reactive;
-using System.Reactive.Linq;
-
-namespace Avalonia.Utilities
-{
-    /// <summary>
-    /// Provides extension methods for working with weak event handlers.
-    /// </summary>
-    public static class WeakObservable
-    {
-
-        private class Handler<TEventArgs> 
-            : IWeakSubscriber<TEventArgs>,
-                IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
-        {
-            private IObserver<EventPattern<object, TEventArgs>> _observer;
-
-            public Handler(IObserver<EventPattern<object, TEventArgs>> observer)
-            {
-                _observer = observer;
-            }
-
-            public void OnEvent(object? sender, TEventArgs e)
-            {
-                _observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
-            }
-
-            public void OnEvent(object? sender, WeakEvent ev, TEventArgs e)
-            {
-                _observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
-            }
-        }
-        
-        /// <summary>
-        /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable
-        /// sequence, subscribing weakly.
-        /// </summary>
-        /// <typeparam name="TTarget">The type of target.</typeparam>
-        /// <typeparam name="TEventArgs">The type of the event args.</typeparam>
-        /// <param name="target">Object instance that exposes the event to convert.</param>
-        /// <param name="ev">The weak event to convert.</param>
-        /// <returns></returns>
-        public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
-            TTarget target, WeakEvent<TTarget, TEventArgs> ev)
-            where TEventArgs : EventArgs where TTarget : class
-        {
-            _ = target ?? throw new ArgumentNullException(nameof(target));
-            _ = ev ?? throw new ArgumentNullException(nameof(ev));
-
-            return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
-            {
-                var handler = new Handler<TEventArgs>(observer);
-                ev.Subscribe(target, handler);
-                return () => ev.Unsubscribe(target, handler);
-            }).Publish().RefCount();
-        }
-
-    }
-}

+ 39 - 36
src/Avalonia.Base/Visual.cs

@@ -11,6 +11,7 @@ using Avalonia.Logging;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Metadata;
+using Avalonia.Reactive;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
 using Avalonia.Rendering.Composition.Server;
@@ -387,52 +388,55 @@ namespace Avalonia
         protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
             where T : Visual
         {
-            static void Invalidate(AvaloniaPropertyChangedEventArgs e)
-            {
-                if (e.Sender is T sender)
+            var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e =>
                 {
-                    sender.InvalidateVisual();
-                }
-            }
-
-            static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e)
-            {
-                if (e.Sender is T sender)
+                    if (e.Sender is T sender)
+                    {
+                        sender.InvalidateVisual();
+                    }
+                });
+            
+            
+            var invalidateAndSubscribeObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
+                static e =>
                 {
-                    if (e.OldValue is IAffectsRender oldValue)
+                    if (e.Sender is T sender)
                     {
-                        if (sender._affectsRenderWeakSubscriber != null)
+                        if (e.OldValue is IAffectsRender oldValue)
                         {
-                            InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber);
+                            if (sender._affectsRenderWeakSubscriber != null)
+                            {
+                                InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber);
+                            }
                         }
-                    }
 
-                    if (e.NewValue is IAffectsRender newValue)
-                    {
-                        if (sender._affectsRenderWeakSubscriber == null)
+                        if (e.NewValue is IAffectsRender newValue)
                         {
-                            sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber<Visual, EventArgs>(
-                                sender, static (target, _, _, _) =>
-                                {
-                                    target.InvalidateVisual();
-                                });
+                            if (sender._affectsRenderWeakSubscriber == null)
+                            {
+                                sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber<Visual, EventArgs>(
+                                    sender, static (target, _, _, _) =>
+                                    {
+                                        target.InvalidateVisual();
+                                    });
+                            }
+                            InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber);
                         }
-                        InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber);
-                    }
 
-                    sender.InvalidateVisual();
-                }
-            }
+                        sender.InvalidateVisual();
+                    }
+                });
 
             foreach (var property in properties)
             {
                 if (property.CanValueAffectRender())
                 {
-                    property.Changed.Subscribe(e => InvalidateAndSubscribe(e));
+                    property.Changed.Subscribe(invalidateAndSubscribeObserver);
                 }
                 else
                 {
-                    property.Changed.Subscribe(e => Invalidate(e));
+                    property.Changed.Subscribe(invalidateObserver);
                 }
             }
         }
@@ -620,23 +624,22 @@ namespace Avalonia
         /// Called when a visual's <see cref="RenderTransform"/> changes.
         /// </summary>
         /// <param name="e">The event args.</param>
-        private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e)
+        private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs<ITransform?> e)
         {
             var sender = e.Sender as Visual;
 
             if (sender?.VisualRoot != null)
             {
-                var oldValue = e.OldValue as Transform;
-                var newValue = e.NewValue as Transform;
+                var (oldValue, newValue) = e.GetOldAndNewValue<ITransform?>();
 
-                if (oldValue != null)
+                if (oldValue is Transform oldTransform)
                 {
-                    oldValue.Changed -= sender.RenderTransformChanged;
+                    oldTransform.Changed -= sender.RenderTransformChanged;
                 }
 
-                if (newValue != null)
+                if (newValue is Transform newTransform)
                 {
-                    newValue.Changed += sender.RenderTransformChanged;
+                    newTransform.Changed += sender.RenderTransformChanged;
                 }
                 
                 sender.InvalidateVisual();

+ 0 - 1
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

@@ -15,7 +15,6 @@
     <!-- Compatibility with old apps -->
     <EmbeddedResource Include="Themes\**\*.xaml" />
   </ItemGroup>
-  <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\EmbedXaml.props" />
   <Import Project="..\..\build\BuildTargets.targets" />
   <!--<Import Project="..\..\build\ApiDiff.props" />-->

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff