diff --git a/.ncrunch/ReactiveUIDemo.v3.ncrunchproject b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject
new file mode 100644
index 0000000000..319cd523ce
--- /dev/null
+++ b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+
+ True
+
+
\ No newline at end of file
diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
index 6ba05332be..3fa8e969c8 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -9,6 +9,7 @@
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
+ "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
diff --git a/Avalonia.sln b/Avalonia.sln
index bd83cde620..7efb294b64 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -232,6 +232,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -543,6 +545,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
+ {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -607,6 +613,7 @@ Global
{47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+ {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index b95b455ca4..4a5f5bc96c 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -135,6 +135,9 @@
+
+
+
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
new file mode 100644
index 0000000000..f3bf1724b4
--- /dev/null
+++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
new file mode 100644
index 0000000000..f9d0328d9a
--- /dev/null
+++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
@@ -0,0 +1,36 @@
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public class RefreshContainerPage : UserControl
+ {
+ private RefreshContainerViewModel _viewModel;
+
+ public RefreshContainerPage()
+ {
+ this.InitializeComponent();
+
+ _viewModel = new RefreshContainerViewModel();
+
+ DataContext = _viewModel;
+ }
+
+ private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
+ {
+ var deferral = e.GetDeferral();
+
+ await _viewModel.AddToTop();
+
+ deferral.Complete();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
new file mode 100644
index 0000000000..d4b43043be
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
@@ -0,0 +1,26 @@
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia.Controls.Notifications;
+using ControlCatalog.Pages;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public class RefreshContainerViewModel : ViewModelBase
+ {
+ public ObservableCollection Items { get; }
+
+ public RefreshContainerViewModel()
+ {
+ Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}"));
+ }
+
+ public async Task AddToTop()
+ {
+ await Task.Delay(3000);
+ Items.Insert(0, $"Item {200 - Items.Count}");
+ }
+ }
+}
diff --git a/samples/ReactiveUIDemo/App.axaml b/samples/ReactiveUIDemo/App.axaml
new file mode 100644
index 0000000000..dd3a39f6ac
--- /dev/null
+++ b/samples/ReactiveUIDemo/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/samples/ReactiveUIDemo/App.axaml.cs b/samples/ReactiveUIDemo/App.axaml.cs
new file mode 100644
index 0000000000..4578566427
--- /dev/null
+++ b/samples/ReactiveUIDemo/App.axaml.cs
@@ -0,0 +1,37 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+using ReactiveUIDemo.ViewModels;
+using ReactiveUIDemo.Views;
+using Splat;
+
+namespace ReactiveUIDemo
+{
+ public class App : Application
+ {
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor));
+ Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor));
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .UseReactiveUI()
+ .LogToTrace();
+ }
+}
diff --git a/samples/ReactiveUIDemo/MainWindow.axaml b/samples/ReactiveUIDemo/MainWindow.axaml
new file mode 100644
index 0000000000..7775fc5a79
--- /dev/null
+++ b/samples/ReactiveUIDemo/MainWindow.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ReactiveUIDemo/MainWindow.axaml.cs b/samples/ReactiveUIDemo/MainWindow.axaml.cs
new file mode 100644
index 0000000000..5bf2d476fd
--- /dev/null
+++ b/samples/ReactiveUIDemo/MainWindow.axaml.cs
@@ -0,0 +1,22 @@
+using ReactiveUIDemo.ViewModels;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ReactiveUIDemo
+{
+ public class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ this.InitializeComponent();
+ this.DataContext = new MainWindowViewModel();
+ this.AttachDevTools();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj
new file mode 100644
index 0000000000..94ca4ee809
--- /dev/null
+++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj
@@ -0,0 +1,28 @@
+
+
+ Exe
+ net6.0
+ enable
+
+
+
+
+
+
+
+
+
+
+ BarView.axaml
+
+
+ FooView.axaml
+
+
+
+
+
+
+
+
+
diff --git a/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
new file mode 100644
index 0000000000..3448453d81
--- /dev/null
+++ b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
@@ -0,0 +1,11 @@
+using ReactiveUI;
+
+namespace ReactiveUIDemo.ViewModels
+{
+ internal class BarViewModel : ReactiveObject, IRoutableViewModel
+ {
+ public BarViewModel(IScreen screen) => HostScreen = screen;
+ public string UrlPathSegment => "Bar";
+ public IScreen HostScreen { get; }
+ }
+}
diff --git a/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
new file mode 100644
index 0000000000..1a363e18dc
--- /dev/null
+++ b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
@@ -0,0 +1,11 @@
+using ReactiveUI;
+
+namespace ReactiveUIDemo.ViewModels
+{
+ internal class FooViewModel : ReactiveObject, IRoutableViewModel
+ {
+ public FooViewModel(IScreen screen) => HostScreen = screen;
+ public string UrlPathSegment => "Foo";
+ public IScreen HostScreen { get; }
+ }
+}
diff --git a/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000000..2222137d38
--- /dev/null
+++ b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,9 @@
+using ReactiveUI;
+
+namespace ReactiveUIDemo.ViewModels
+{
+ internal class MainWindowViewModel : ReactiveObject
+ {
+ public RoutedViewHostPageViewModel RoutedViewHost { get; } = new();
+ }
+}
diff --git a/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
new file mode 100644
index 0000000000..701447cfe8
--- /dev/null
+++ b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
@@ -0,0 +1,21 @@
+using ReactiveUI;
+
+namespace ReactiveUIDemo.ViewModels
+{
+ internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen
+ {
+ public RoutedViewHostPageViewModel()
+ {
+ Foo = new(this);
+ Bar = new(this);
+ Router.Navigate.Execute(Foo);
+ }
+
+ public RoutingState Router { get; } = new();
+ public FooViewModel Foo { get; }
+ public BarViewModel Bar { get; }
+
+ public void ShowFoo() => Router.Navigate.Execute(Foo);
+ public void ShowBar() => Router.Navigate.Execute(Bar);
+ }
+}
diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml b/samples/ReactiveUIDemo/Views/BarView.axaml
new file mode 100644
index 0000000000..2622245997
--- /dev/null
+++ b/samples/ReactiveUIDemo/Views/BarView.axaml
@@ -0,0 +1,16 @@
+
+
+
+ Bar!
+
+
+
diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml.cs b/samples/ReactiveUIDemo/Views/BarView.axaml.cs
new file mode 100644
index 0000000000..2fbea6de91
--- /dev/null
+++ b/samples/ReactiveUIDemo/Views/BarView.axaml.cs
@@ -0,0 +1,28 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ReactiveUI;
+using ReactiveUIDemo.ViewModels;
+
+namespace ReactiveUIDemo.Views
+{
+ internal partial class BarView : UserControl, IViewFor
+ {
+ public BarView()
+ {
+ InitializeComponent();
+ }
+
+ public BarViewModel? ViewModel { get; set; }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (BarViewModel?)value;
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml b/samples/ReactiveUIDemo/Views/FooView.axaml
new file mode 100644
index 0000000000..8f73250d3b
--- /dev/null
+++ b/samples/ReactiveUIDemo/Views/FooView.axaml
@@ -0,0 +1,16 @@
+
+
+
+ Foo!
+
+
+
diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml.cs b/samples/ReactiveUIDemo/Views/FooView.axaml.cs
new file mode 100644
index 0000000000..313a71044c
--- /dev/null
+++ b/samples/ReactiveUIDemo/Views/FooView.axaml.cs
@@ -0,0 +1,28 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ReactiveUI;
+using ReactiveUIDemo.ViewModels;
+
+namespace ReactiveUIDemo.Views
+{
+ internal partial class FooView : UserControl, IViewFor
+ {
+ public FooView()
+ {
+ InitializeComponent();
+ }
+
+ public FooViewModel? ViewModel { get; set; }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (FooViewModel?)value;
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
index 57a5c7101f..8fbfa854b1 100644
--- a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
+++ b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
@@ -90,7 +90,7 @@ namespace RenderDemo.Pages
return new ControlRun(_control, _defaultProperties);
}
- return new TextCharacters(_text.AsMemory(), _defaultProperties);
+ return new TextCharacters(_text, _defaultProperties);
}
}
diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
index 8765cfb4c9..b5d1feb4a7 100644
--- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
@@ -39,7 +40,7 @@ namespace Avalonia.Animation.Animators
VerifyConvertKeyFrames();
var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete);
- return match.Subscribe(subject);
+ return new CompositeDisposable(match.Subscribe(subject), subject);
}
protected T InterpolationHandler(double animationTime, T neutralValue)
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
new file mode 100644
index 0000000000..fedd07ec32
--- /dev/null
+++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
@@ -0,0 +1,152 @@
+using Avalonia.Input.GestureRecognizers;
+
+namespace Avalonia.Input
+{
+ public class PullGestureRecognizer : StyledElement, IGestureRecognizer
+ {
+ private IInputElement? _target;
+ private IGestureRecognizerActionsDispatcher? _actions;
+ private Point _initialPosition;
+ private int _gestureId;
+ private IPointer? _tracking;
+ private PullDirection _pullDirection;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty PullDirectionProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(PullDirection),
+ o => o.PullDirection,
+ (o, v) => o.PullDirection = v);
+
+ public PullDirection PullDirection
+ {
+ get => _pullDirection;
+ set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
+ }
+
+ public PullGestureRecognizer(PullDirection pullDirection)
+ {
+ PullDirection = pullDirection;
+ }
+
+ public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
+ {
+ _target = target;
+ _actions = actions;
+
+ _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
+ _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
+ }
+
+ private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ PointerPressed(e);
+ }
+
+ private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ PointerReleased(e);
+ }
+
+ public void PointerCaptureLost(IPointer pointer)
+ {
+ if (_tracking == pointer)
+ {
+ EndPull();
+ }
+ }
+
+ public void PointerMoved(PointerEventArgs e)
+ {
+ if (_tracking == e.Pointer && _target is Visual visual)
+ {
+ var currentPosition = e.GetPosition(visual);
+ _actions!.Capture(e.Pointer, this);
+
+ Vector delta = default;
+ switch (PullDirection)
+ {
+ case PullDirection.TopToBottom:
+ if (currentPosition.Y > _initialPosition.Y)
+ {
+ delta = new Vector(0, currentPosition.Y - _initialPosition.Y);
+ }
+ break;
+ case PullDirection.BottomToTop:
+ if (currentPosition.Y < _initialPosition.Y)
+ {
+ delta = new Vector(0, _initialPosition.Y - currentPosition.Y);
+ }
+ break;
+ case PullDirection.LeftToRight:
+ if (currentPosition.X > _initialPosition.X)
+ {
+ delta = new Vector(currentPosition.X - _initialPosition.X, 0);
+ }
+ break;
+ case PullDirection.RightToLeft:
+ if (currentPosition.X < _initialPosition.X)
+ {
+ delta = new Vector(_initialPosition.X - currentPosition.X, 0);
+ }
+ break;
+ }
+
+ _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
+ }
+ }
+
+ public void PointerPressed(PointerPressedEventArgs e)
+ {
+ if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
+ {
+ var position = e.GetPosition(visual);
+
+ var canPull = false;
+
+ var bounds = visual.Bounds;
+
+ switch (PullDirection)
+ {
+ case PullDirection.TopToBottom:
+ canPull = position.Y < bounds.Height * 0.1;
+ break;
+ case PullDirection.BottomToTop:
+ canPull = position.Y > bounds.Height - (bounds.Height * 0.1);
+ break;
+ case PullDirection.LeftToRight:
+ canPull = position.X < bounds.Width * 0.1;
+ break;
+ case PullDirection.RightToLeft:
+ canPull = position.X > bounds.Width - (bounds.Width * 0.1);
+ break;
+ }
+
+ if (canPull)
+ {
+ _gestureId = PullGestureEventArgs.GetNextFreeId();
+ _tracking = e.Pointer;
+ _initialPosition = position;
+ }
+ }
+ }
+
+ public void PointerReleased(PointerReleasedEventArgs e)
+ {
+ if (_tracking == e.Pointer)
+ {
+ EndPull();
+ }
+ }
+
+ private void EndPull()
+ {
+ _tracking = null;
+ _initialPosition = default;
+
+ _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs
index 496052ad35..1ea88fe824 100644
--- a/src/Avalonia.Base/Input/Gestures.cs
+++ b/src/Avalonia.Base/Input/Gestures.cs
@@ -46,6 +46,14 @@ namespace Avalonia.Input
private static readonly WeakReference