diff --git a/NOTICE.md b/NOTICE.md index bd26b65d70..7083706c3e 100644 --- a/NOTICE.md +++ b/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. diff --git a/build/Base.props b/build/Base.props index 9ec1c3c2d3..26f19e3abc 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,7 @@  + diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs index 52c122f2bc..32cfa9b22b 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs +++ b/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; diff --git a/samples/ControlCatalog/Pages/PointerContactsTab.cs b/samples/ControlCatalog/Pages/PointerContactsTab.cs index b6aabebf99..1751b046c0 100644 --- a/samples/ControlCatalog/Pages/PointerContactsTab.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs index bbe970afd6..d3e4ea7c31 100644 --- a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs index 4a5462a58b..f041f32b10 100644 --- a/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index f89d9d1e20..7f32536b11 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 47c6f70714..3628a9b8a7 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index 16051c3c05..df62ba04cb 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs index a31f164a2a..bcbcb345ef 100644 --- a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs +++ b/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 diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs index d4b43043be..aa15d7758b 100644 --- a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs +++ b/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; diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index 80d4844f7a..7c0855e0af 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/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; diff --git a/samples/MiniMvvm/MiniMvvm.csproj b/samples/MiniMvvm/MiniMvvm.csproj index 6535b2bdbd..2a9164624a 100644 --- a/samples/MiniMvvm/MiniMvvm.csproj +++ b/samples/MiniMvvm/MiniMvvm.csproj @@ -2,5 +2,7 @@ netstandard2.0 - + + + diff --git a/samples/MiniMvvm/PropertyChangedExtensions.cs b/samples/MiniMvvm/PropertyChangedExtensions.cs index f1065c7530..f51773810d 100644 --- a/samples/MiniMvvm/PropertyChangedExtensions.cs +++ b/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> v3, Func 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> WhenAnyValue(this TModel model, Expression> v1, diff --git a/samples/MiniMvvm/ViewModelBase.cs b/samples/MiniMvvm/ViewModelBase.cs index 7256b05cef..8bc398607f 100644 --- a/samples/MiniMvvm/ViewModelBase.cs +++ b/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 diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 5c743aabdb..5f61a08f3c 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -11,4 +11,5 @@ + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 2cc84168dc..76c1ba7b69 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -12,7 +12,8 @@ - + + diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj index 94ca4ee809..9650068434 100644 --- a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -23,6 +23,5 @@ - diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 223a53d8c5..1643ca3ee2 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -15,6 +15,4 @@ ControlCatalog - - diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index de9149e9a1..152076013f 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/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; diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 19dc7b4ab6..3545ae8fe1 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/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; diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index 06087cdd6a..d62acc0d52 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/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; diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 6a6e69894b..682629c801 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/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 { diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs index 3168a67d79..dcce75b31c 100644 --- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs +++ b/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); } } diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index b5d1feb4a7..2db890bd0a 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/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; diff --git a/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs b/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs index 72add21d69..7be974b9e5 100644 --- a/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs +++ b/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; diff --git a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs index e12ca722f9..8040fb595b 100644 --- a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs +++ b/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; diff --git a/src/Avalonia.Base/Animation/Clock.cs b/src/Avalonia.Base/Animation/Clock.cs index 5afd2ae705..f2bce9d3a5 100644 --- a/src/Avalonia.Base/Animation/Clock.cs +++ b/src/Avalonia.Base/Animation/Clock.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; namespace Avalonia.Animation { diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index a229bc7ce6..640d6456a3 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/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(); - using (var disposables = new CompositeDisposable()) + using (var disposables = new CompositeDisposable(1)) { if (to != null) { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index d6f1542687..cd122a8b67 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -14,7 +14,6 @@ - @@ -37,6 +36,13 @@ + + + + + + + @@ -48,8 +54,12 @@ + + + + diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 867d6215a5..7b17b9152d 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/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))); } - /// - /// Gets a subject for an . - /// - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject GetSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create( - Observer.Create(x => o.SetValue(property, x, priority)), - o.GetObservable(property)); - } - - /// - /// Gets a subject for an . - /// - /// The property type. - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject GetSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create( - Observer.Create(x => o.SetValue(property, x, priority)), - o.GetObservable(property)); - } - - /// - /// Gets a subject for a . - /// - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject> GetBindingSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create>( - Observer.Create>(x => - { - if (x.HasValue) - { - o.SetValue(property, x.Value, priority); - } - }), - o.GetBindingObservable(property)); - } - - /// - /// Gets a subject for a . - /// - /// The property type. - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject> GetBindingSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create>( - Observer.Create>(x => - { - if (x.HasValue) - { - o.SetValue(property, x.Value, priority); - } - }), - o.GetBindingObservable(property)); - } - /// /// Binds an to an observable. /// @@ -407,13 +301,7 @@ namespace Avalonia Action action) where TTarget : AvaloniaObject { - return observable.Subscribe(e => - { - if (e.Sender is TTarget target) - { - action(target, e); - } - }); + return observable.Subscribe(new ClassHandlerObserver(action)); } /// @@ -431,13 +319,7 @@ namespace Avalonia this IObservable> observable, Action> action) where TTarget : AvaloniaObject { - return observable.Subscribe(e => - { - if (e.Sender is TTarget target) - { - action(target, e); - } - }); + return observable.Subscribe(new ClassHandlerObserver(action)); } private class BindingAdaptor : IBinding @@ -458,5 +340,57 @@ namespace Avalonia return InstancedBinding.OneWay(_source); } } + + private class ClassHandlerObserver : IObserver> + { + private readonly Action> _action; + + public ClassHandlerObserver(Action> 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); + } + } + } + + private class ClassHandlerObserver : IObserver + { + private readonly Action _action; + + public ClassHandlerObserver(Action 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); + } + } + } } } diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 53444ee475..f8c062a176 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/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 /// The value type of the property. public abstract class AvaloniaProperty : AvaloniaProperty { - private readonly Subject> _changed; + private readonly LightweightSubject> _changed; /// /// Initializes a new instance of the class. @@ -28,7 +28,7 @@ namespace Avalonia Action? notifying = null) : base(name, typeof(TValue), ownerType, metadata, notifying) { - _changed = new Subject>(); + _changed = new LightweightSubject>(); } /// diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index 589b9b2d01..a9726cb86e 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index c4684960d6..2178577eb7 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/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 { diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index 689fcc89a4..68863ea257 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/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; diff --git a/src/Avalonia.Base/Controls/NameScopeLocator.cs b/src/Avalonia.Base/Controls/NameScopeLocator.cs index f0ce7f8a5b..371931f971 100644 --- a/src/Avalonia.Base/Controls/NameScopeLocator.cs +++ b/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 diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index ceb3f71285..0b737dd959 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/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; diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index e340966983..536c14dcf9 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Linq; using Avalonia.Reactive; namespace Avalonia.Data.Core diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index f60b4722d9..55caf8070e 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/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. /// [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] - public class BindingExpression : LightweightObservableBase, ISubject, IDescription + public class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 2151c100e5..0818b5fa62 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/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 /// /// A function which gets the root object. /// The expression. - /// An observable which triggers a re-read of the getter. + /// An observable which triggers a re-read of the getter. Generic argument value is not used. /// /// A description of the expression. /// public ExpressionObserver( Func rootGetter, ExpressionNode node, - IObservable update, + IObservable update, string? description) { Description = description; @@ -164,7 +162,7 @@ namespace Avalonia.Data.Core /// /// A function which gets the root object. /// The expression. - /// An observable which triggers a re-read of the getter. + /// An observable which triggers a re-read of the getter. Generic argument value is not used. /// Whether or not to track data validation /// /// A description of the expression. If null, 's string representation will be used. @@ -173,7 +171,7 @@ namespace Avalonia.Data.Core public static ExpressionObserver Create( Func rootGetter, Expression> expression, - IObservable update, + IObservable update, bool enableDataValidation = false, string? description = null) { @@ -296,9 +294,10 @@ namespace Avalonia.Data.Core if (_root is IObservable observable) { _rootSubscription = observable.Subscribe( - x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), - x => PublishCompleted(), - () => PublishCompleted()); + new AnonymousObserver( + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + x => PublishCompleted(), + PublishCompleted)); } else { diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index a808827896..2fad96701d 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/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, + IWeakEventSubscriber { - private IDisposable? _subscription; - protected override void StartListeningCore(WeakReference reference) { reference.TryGetTarget(out var target); - var incc = target as INotifyCollectionChanged; - var inpc = target as INotifyPropertyChanged; - var inputs = new List>(); - - 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.OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs e) + { + if (ShouldUpdate(sender, e)) + { + ValueChanged(GetValue(sender)); + } + } + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e) + { + if (ShouldUpdate(sender, e)) + { + ValueChanged(GetValue(sender)); + } + } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index ebee4586db..b40628fd35 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/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() + { + + } + /// /// Checks whether this plugin handles the specified value. /// @@ -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 method. + var select = GetBoxObservable(sourceType); - // Call Observable.Select(target, box); + // Call BoxObservable(target); return (IObservable)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 value) => (object?)value; + private static IObservable BoxObservable(IObservable source) + { + return source.Select(v => (object?)v); + } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 5203aa9f57..715f4604cf 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/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(); + var subject = new LightweightSubject(); task.ContinueWith( x => HandleCompleted(task).Subscribe(subject), TaskScheduler.FromCurrentSynchronizationContext()) diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 6dc6d07184..ba18a2173b 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/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 { diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index fcd179b9b2..83ef8f76b4 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/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( + new AnonymousObserver(x => Source.SetValue(Property, x, BindingPriority.LocalValue)), + Source.GetObservable(Property)); + return new InstancedBinding(subject, Mode, BindingPriority.LocalValue); } } } diff --git a/src/Avalonia.Base/Data/IndexerDescriptor.cs b/src/Avalonia.Base/Data/IndexerDescriptor.cs index c823630d3c..3eb5a6ef12 100644 --- a/src/Avalonia.Base/Data/IndexerDescriptor.cs +++ b/src/Avalonia.Base/Data/IndexerDescriptor.cs @@ -1,12 +1,11 @@ using System; -using System.Reactive; namespace Avalonia.Data { /// /// Holds a description of a binding for 's [] operator. /// - public class IndexerDescriptor : ObservableBase, IDescription + public class IndexerDescriptor : IObservable, IDescription { /// /// Gets or sets the binding mode. @@ -104,7 +103,7 @@ namespace Avalonia.Data } /// - protected override IDisposable SubscribeCore(IObserver observer) + public IDisposable Subscribe(IObserver observer) { if (SourceObservable is null && Source is null) throw new InvalidOperationException("Cannot subscribe to IndexerDescriptor."); diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index 4a1e2660de..a60c1d72ec 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/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 /// public class InstancedBinding { - /// - /// Initializes a new instance of the class. - /// - /// The binding source. - /// The binding mode. - /// The priority of the binding. - /// - /// This constructor can be used to create any type of binding and as such requires an - /// 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. - /// - public InstancedBinding(ISubject 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? Observable => Value as IObservable; /// - /// Gets the as a subject. + /// Gets the as an observer. + /// + public IObserver? Observer => Value as IObserver; + + /// + /// Gets the as an subject. /// - public ISubject? Subject => Value as ISubject; + internal IAvaloniaSubject? Subject => Value as IAvaloniaSubject; /// /// Creates a new one-time binding with a fixed value. @@ -111,30 +97,34 @@ namespace Avalonia.Data /// /// Creates a new one-way to source binding. /// - /// The binding source. + /// The binding source. /// The priority of the binding. /// An instance. public static InstancedBinding OneWayToSource( - ISubject subject, + IObserver 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); } /// /// Creates a new two-way binding. /// - /// The binding source. + /// The binding source. + /// The binding source. /// The priority of the binding. /// An instance. public static InstancedBinding TwoWay( - ISubject subject, + IObservable observable, + IObserver 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(observer, observable); return new InstancedBinding(subject, BindingMode.TwoWay, priority); } diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 54e61d89b2..a9e42c2374 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/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 diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index f233fdce51..962c7aa334 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/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 diff --git a/src/Avalonia.Base/Input/InputManager.cs b/src/Avalonia.Base/Input/InputManager.cs index f604ff8e74..e87665fd54 100644 --- a/src/Avalonia.Base/Input/InputManager.cs +++ b/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 /// public class InputManager : IInputManager { - private readonly Subject _preProcess = new Subject(); - private readonly Subject _process = new Subject(); - private readonly Subject _postProcess = new Subject(); + private readonly LightweightSubject _preProcess = new(); + private readonly LightweightSubject _process = new(); + private readonly LightweightSubject _postProcess = new(); /// /// Gets the global instance of the input manager. diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 9b17e336d3..e1c42c4ead 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/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; diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs index 8a3f5b968e..9b5668bf98 100644 --- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs @@ -1,5 +1,5 @@ using System; -using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Input.TextInput { diff --git a/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs b/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs index b12cf64fdf..5264b96679 100644 --- a/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs +++ b/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 { diff --git a/src/Avalonia.Base/Interactivity/RoutedEvent.cs b/src/Avalonia.Base/Interactivity/RoutedEvent.cs index 7046871538..79a4dc60e4 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEvent.cs +++ b/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 _routeFinished = new Subject(); + private readonly LightweightSubject<(object, RoutedEventArgs)> _raised = new(); + private readonly LightweightSubject _routeFinished = new(); public RoutedEvent( string name, diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index d185148894..775b8adddd 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/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(params AvaloniaProperty[] properties) where T : Layoutable { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.InvalidateMeasure(); - } + var invalidateObserver = new AnonymousObserver( + 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(params AvaloniaProperty[] properties) where T : Layoutable { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.InvalidateArrange(); - } + var invalidate = new AnonymousObserver( + static e => (e.Sender as T)?.InvalidateArrange()); foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + property.Changed.Subscribe(invalidate); } } diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index 4138f1c891..b9a560ad8f 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/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(params AvaloniaProperty[] properties) where T : Brush { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); foreach (var property in properties) { - property.Changed.Subscribe(e => Invalidate(e)); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 9c30b6f872..4749bfa401 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/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( + 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); } /// diff --git a/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs index 0e485d0db8..22d5a29870 100644 --- a/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs +++ b/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(params AvaloniaProperty[] properties) where T : ExperimentalAcrylicMaterial { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); foreach (var property in properties) { - property.Changed.Subscribe(e => Invalidate(e)); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index 2019f54c70..9ef32c7d09 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/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 /// protected static void AffectsGeometry(params AvaloniaProperty[] properties) { + var invalidateObserver = new AnonymousObserver(AffectsGeometryInvalidate); foreach (var property in properties) { - property.Changed.Subscribe(AffectsGeometryInvalidate); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index 83cdaa1694..e1654a01b2 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/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 { diff --git a/src/Avalonia.Base/Media/MatrixTransform.cs b/src/Avalonia.Base/Media/MatrixTransform.cs index c61acb730c..22f39b1ee2 100644 --- a/src/Avalonia.Base/Media/MatrixTransform.cs +++ b/src/Avalonia.Base/Media/MatrixTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media diff --git a/src/Avalonia.Base/Media/RotateTransform.cs b/src/Avalonia.Base/Media/RotateTransform.cs index 3bd409149c..ab3ea6770f 100644 --- a/src/Avalonia.Base/Media/RotateTransform.cs +++ b/src/Avalonia.Base/Media/RotateTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media diff --git a/src/Avalonia.Base/Media/ScaleTransform.cs b/src/Avalonia.Base/Media/ScaleTransform.cs index d4c1a7f993..fca2f4bf2a 100644 --- a/src/Avalonia.Base/Media/ScaleTransform.cs +++ b/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 /// public ScaleTransform() { - this.GetObservable(ScaleXProperty).Subscribe(_ => RaiseChanged()); - this.GetObservable(ScaleYProperty).Subscribe(_ => RaiseChanged()); } /// @@ -63,5 +62,15 @@ namespace Avalonia.Media /// Gets the transform's . /// 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(); + } + } } } diff --git a/src/Avalonia.Base/Media/SkewTransform.cs b/src/Avalonia.Base/Media/SkewTransform.cs index 066f5371c3..d268bee778 100644 --- a/src/Avalonia.Base/Media/SkewTransform.cs +++ b/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 /// public SkewTransform() { - this.GetObservable(AngleXProperty).Subscribe(_ => RaiseChanged()); - this.GetObservable(AngleYProperty).Subscribe(_ => RaiseChanged()); } /// @@ -62,5 +61,15 @@ namespace Avalonia.Media /// Gets the transform's . /// 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(); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs index f5d39e4371..499026e8b3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ b/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 /// /// Gets a span from the character buffer range /// - public ReadOnlySpan Span => - CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); + public ReadOnlySpan Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); /// /// Gets the character memory buffer @@ -174,7 +173,7 @@ namespace Avalonia.Media.TextFormatting /// /// Indicate whether the character buffer range is empty /// - 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); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index f677617b14..01804e1ce3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract List? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 086ea85d97..9c201bda22 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { - if (textLine.TextRuns is not List 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(0); + return new List(0); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(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(textRuns.Count); + var collapsedRuns = new List(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; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index ef2abdfea0..989bf7749d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/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 drawableTextRuns; + IReadOnlyList 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 /// The text run's. /// The length to split at. /// The split text runs. - internal static SplitResult> SplitDrawableRuns(List textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList 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(firstCount); + var first = new List(firstCount); if (firstCount > 1) { @@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.Length == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { @@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = new List(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>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + return new SplitResult>(textRuns, null); } /// @@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var drawableTextRuns = new List(); + var shapedRuns = new List(); 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(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 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 textRuns, double paragraphWidth, out int measuredLength) + private static bool TryMeasureLength(IReadOnlyList 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 /// /// The current line break if the line was explicitly broken. /// The wrapped text line. - private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex, + private static TextLineImpl PerformTextWrapping(IReadOnlyList 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) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f803001481..ef0c726793 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/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 diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 7b80d5ce40..e30a0fe9f4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { - if (textLine.TextRuns is not List 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(0); + return new List(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(textRuns.Count); + currentWidth += shapedRun.Size.Width; - if (measuredLength > 0) + if (currentWidth > availableWidth) { - List? preSplitRuns = null; - List? 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(textRuns.Count); - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; - } - else + if (measuredLength > 0) { - postSplitRuns = textRuns; - } + IReadOnlyList? preSplitRuns = null; + IReadOnlyList? 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++; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs index ce35e47fbd..bf26ac5df4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs +++ b/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? remainingRuns = null) + IReadOnlyList? remainingRuns = null) { TextEndOfLine = textEndOfLine; FlowDirection = flowDirection; @@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting /// /// Get the remaining runs that were split up by the during the formatting process. /// - public IReadOnlyList? RemainingRuns { get; } + public IReadOnlyList? RemainingRuns { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 5fb1171221..a1f93bcd07 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -6,13 +6,13 @@ namespace Avalonia.Media.TextFormatting { internal class TextLineImpl : TextLine { - private readonly List _textRuns; + private IReadOnlyList _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(IReadOnlyList 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(_textRuns.Count); current = orderedRun; while (current != null) { - _textRuns.Add(current.Run); + textRuns.Add(current.Run); current = current.Next; } + + _textRuns = textRuns; } /// @@ -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; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index e79c2ed8b3..56232ec9c8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/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); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 1de04ad061..deecbbe476 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index 7c94715aa4..c291e1dfb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } diff --git a/src/Avalonia.Base/Media/TranslateTransform.cs b/src/Avalonia.Base/Media/TranslateTransform.cs index 0f910f3600..1d0169a62c 100644 --- a/src/Avalonia.Base/Media/TranslateTransform.cs +++ b/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 /// public TranslateTransform() { - this.GetObservable(XProperty).Subscribe(_ => RaiseChanged()); - this.GetObservable(YProperty).Subscribe(_ => RaiseChanged()); } /// @@ -63,5 +62,15 @@ namespace Avalonia.Media /// Gets the transform's . /// 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(); + } + } } } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index b5ac5c817d..11dc80ef8f 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/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; diff --git a/src/Avalonia.Base/Reactive/AnonymousObserver.cs b/src/Avalonia.Base/Reactive/AnonymousObserver.cs new file mode 100644 index 0000000000..c2e02ae879 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AnonymousObserver.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.Reactive; + +internal class AnonymousObserver : IObserver +{ + private static readonly Action ThrowsOnError = ex => throw ex; + private static readonly Action NoOpCompleted = () => { }; + private readonly Action _onNext; + private readonly Action _onError; + private readonly Action _onCompleted; + + public AnonymousObserver(TaskCompletionSource tcs) + { + if (tcs is null) + { + throw new ArgumentNullException(nameof(tcs)); + } + + _onNext = tcs.SetResult; + _onError = tcs.SetException; + _onCompleted = NoOpCompleted; + } + + public AnonymousObserver(Action onNext, Action 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 onNext) + : this(onNext, ThrowsOnError, NoOpCompleted) + { + } + + public AnonymousObserver(Action onNext, Action onError) + : this(onNext, onError, NoOpCompleted) + { + } + + public AnonymousObserver(Action 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); + } +} diff --git a/src/Avalonia.Base/Reactive/CombinedSubject.cs b/src/Avalonia.Base/Reactive/CombinedSubject.cs new file mode 100644 index 0000000000..dc6153133d --- /dev/null +++ b/src/Avalonia.Base/Reactive/CombinedSubject.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Reactive; + +internal class CombinedSubject : IAvaloniaSubject +{ + private readonly IObserver _observer; + private readonly IObservable _observable; + + public CombinedSubject(IObserver observer, IObservable 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 observer) => _observable.Subscribe(observer); +} diff --git a/src/Avalonia.Base/Reactive/CompositeDisposable.cs b/src/Avalonia.Base/Reactive/CompositeDisposable.cs new file mode 100644 index 0000000000..4952686ad8 --- /dev/null +++ b/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 +{ + private readonly object _gate = new object(); + private bool _disposed; + private List _disposables; + private int _count; + private const int ShrinkThreshold = 64; + + /// + /// Initializes a new instance of the class with the specified number of disposables. + /// + /// The number of disposables that the new CompositeDisposable can initially store. + /// is less than zero. + public CompositeDisposable(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + _disposables = new List(capacity); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + /// Any of the disposables in the collection is null. + 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); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + /// Any of the disposables in the collection is null. + public CompositeDisposable(IList 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 ToList(IEnumerable disposables) + { + var capacity = disposables switch + { + IDisposable[] a => a.Length, + ICollection c => c.Count, + _ => 12 + }; + + var list = new List(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; + } + + /// + /// Gets the number of disposables contained in the . + /// + public int Count => Volatile.Read(ref _count); + + /// + /// Adds a disposable to the or disposes the disposable if the is disposed. + /// + /// Disposable to add. + /// is null. + 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(); + } + + /// + /// Removes and disposes the first occurrence of a disposable from the . + /// + /// Disposable to remove. + /// true if found; false otherwise. + /// is null. + 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 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(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; + } + + /// + /// Disposes all disposables in the group and removes them from the group. + /// + public void Dispose() + { + List? 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(); + } + } + } + + /// + /// Removes and disposes all disposables from the , but does not dispose the . + /// + 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(); + } + } + + /// + /// Determines whether the contains a specific disposable. + /// + /// Disposable to search for. + /// true if the disposable was found; otherwise, false. + /// is null. + public bool Contains(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + if (_disposed) + { + return false; + } + + return _disposables.Contains(item); + } + } + + /// + /// Copies the disposables contained in the to an array, starting at a particular array index. + /// + /// Array to copy the contained disposables to. + /// Target index at which to copy the first disposable of the group. + /// is null. + /// is less than zero. -or - is larger than or equal to the array length. + 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; + } + } + } + } + + /// + /// Always returns false. + /// + public bool IsReadOnly => false; + + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator to iterate over the disposables. + public IEnumerator 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()); + } + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator to iterate over the disposables. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed => Volatile.Read(ref _disposed); + + /// + /// An empty enumerator for the + /// method to avoid allocation on disposed or empty composites. + /// + private static readonly CompositeEnumerator EmptyEnumerator = + new CompositeEnumerator(Array.Empty()); + + /// + /// An enumerator for an array of disposables. + /// + private sealed class CompositeEnumerator : IEnumerator + { + 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; + } + } +} diff --git a/src/Avalonia.Base/Reactive/Disposable.cs b/src/Avalonia.Base/Reactive/Disposable.cs new file mode 100644 index 0000000000..17b0d01db5 --- /dev/null +++ b/src/Avalonia.Base/Reactive/Disposable.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive; + +/// +/// Provides a set of static methods for creating objects. +/// +internal static class Disposable +{ + /// + /// Represents a disposable that does nothing on disposal. + /// + 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 : IDisposable + { + private TState _state; + private volatile Action? _dispose; + + public AnonymousDisposable(TState state, Action dispose) + { + _state = state; + _dispose = dispose; + } + + public bool IsDisposed => _dispose == null; + public void Dispose() + { + Interlocked.Exchange(ref _dispose, null)?.Invoke(_state); + _state = default!; + } + } + + /// + /// Gets the disposable that does nothing when disposed. + /// + public static IDisposable Empty => EmptyDisposable.Instance; + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + + return new AnonymousDisposable(dispose); + } + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// The state to be passed to the action. + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(TState state, Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + + return new AnonymousDisposable(state, dispose); + } +} diff --git a/src/Avalonia.Base/Reactive/DisposableMixin.cs b/src/Avalonia.Base/Reactive/DisposableMixin.cs new file mode 100644 index 0000000000..478312fa47 --- /dev/null +++ b/src/Avalonia.Base/Reactive/DisposableMixin.cs @@ -0,0 +1,37 @@ +using System; +using Avalonia.Reactive; + +namespace Avalonia.Reactive; + +/// +/// Extension methods associated with the IDisposable interface. +/// +internal static class DisposableMixin +{ + /// + /// Ensures the provided disposable is disposed with the specified . + /// + /// + /// The type of the disposable. + /// + /// + /// The disposable we are going to want to be disposed by the CompositeDisposable. + /// + /// + /// The to which will be added. + /// + /// + /// The disposable. + /// + public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) + where T : IDisposable + { + if (compositeDisposable is null) + { + throw new ArgumentNullException(nameof(compositeDisposable)); + } + + compositeDisposable.Add(item); + return item; + } +} diff --git a/src/Avalonia.Base/Reactive/IAvaloniaSubject.cs b/src/Avalonia.Base/Reactive/IAvaloniaSubject.cs new file mode 100644 index 0000000000..272e2190b8 --- /dev/null +++ b/src/Avalonia.Base/Reactive/IAvaloniaSubject.cs @@ -0,0 +1,8 @@ +using System; + +namespace Avalonia.Reactive; + +internal interface IAvaloniaSubject : IObserver, IObservable /*, ISubject */ +{ + +} diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 9a3ab89b62..263109972f 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/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 /// /// The observable type. /// - /// 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. /// @@ -21,11 +19,13 @@ namespace Avalonia.Reactive private Exception? _error; private List>? _observers = new List>(); + public bool HasObservers => _observers?.Count > 0; + public IDisposable Subscribe(IObserver observer) { _ = observer ?? throw new ArgumentNullException(nameof(observer)); - Dispatcher.UIThread.VerifyAccess(); + //Dispatcher.UIThread.VerifyAccess(); var first = false; diff --git a/src/Avalonia.Base/Reactive/LightweightSubject.cs b/src/Avalonia.Base/Reactive/LightweightSubject.cs new file mode 100644 index 0000000000..5663fd66d3 --- /dev/null +++ b/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 : LightweightObservableBase, IAvaloniaSubject +{ + 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() { } +} diff --git a/src/Avalonia.Base/Reactive/Observable.cs b/src/Avalonia.Base/Reactive/Observable.cs new file mode 100644 index 0000000000..1009829429 --- /dev/null +++ b/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; + +/// +/// Provides common observable methods as a replacement for the Rx framework. +/// +internal static class Observable +{ + public static IObservable Create(Func, IDisposable> subscribe) + { + return new CreateWithDisposableObservable(subscribe); + } + + public static IDisposable Subscribe(this IObservable source, Action action) + { + return source.Subscribe(new AnonymousObserver(action)); + } + + public static IObservable Select(this IObservable source, Func selector) + { + return Create(obs => + { + return source.Subscribe(new AnonymousObserver( + input => + { + TResult value; + try + { + value = selector(input); + } + catch (Exception ex) + { + obs.OnError(ex); + return; + } + + obs.OnNext(value); + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable StartWith(this IObservable source, TSource value) + { + return Create(obs => + { + obs.OnNext(value); + return source.Subscribe(obs); + }); + } + + public static IObservable Where(this IObservable source, Func predicate) + { + return Create(obs => + { + return source.Subscribe(new AnonymousObserver( + 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 Switch( + this IObservable> sources) + { + return new Switch(sources); + } + + public static IObservable CombineLatest( + this IObservable first, IObservable second, + Func resultSelector) + { + return new CombineLatest(first, second, resultSelector); + } + + public static IObservable CombineLatest( + this IEnumerable> inputs) + { + return new CombineLatest(inputs, items => items); + } + + public static IObservable Skip(this IObservable source, int skipCount) + { + if (skipCount <= 0) + { + throw new ArgumentException("Skip count must be bigger than zero", nameof(skipCount)); + } + + return Create(obs => + { + var remaining = skipCount; + return source.Subscribe(new AnonymousObserver( + input => + { + if (remaining <= 0) + { + obs.OnNext(input); + } + else + { + remaining--; + } + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable Take(this IObservable source, int takeCount) + { + if (takeCount <= 0) + { + return Empty(); + } + + return Create(obs => + { + var remaining = takeCount; + IDisposable? sub = null; + sub = source.Subscribe(new AnonymousObserver( + input => + { + if (remaining > 0) + { + --remaining; + obs.OnNext(input); + + if (remaining == 0) + { + sub?.Dispose(); + obs.OnCompleted(); + } + } + }, obs.OnError, obs.OnCompleted)); + return sub; + }); + } + + public static IObservable FromEventPattern(Action addHandler, Action removeHandler) + { + return Create(observer => + { + var handler = new Action(observer.OnNext); + var converted = new EventHandler((_, args) => handler(args)); + addHandler(converted); + + return Disposable.Create(() => removeHandler(converted)); + }); + } + + public static IObservable Return(T value) + { + return new ReturnImpl(value); + } + + public static IObservable Empty() + { + return EmptyImpl.Instance; + } + + /// + /// Returns an observable that fires once with the specified value and never completes. + /// + /// The type of the value. + /// The value. + /// The observable. + public static IObservable SingleValue(T value) + { + return new SingleValueImpl(value); + } + + private sealed class SingleValueImpl : IObservable + { + private readonly T _value; + + public SingleValueImpl(T value) + { + _value = value; + } + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + return Disposable.Empty; + } + } + + private sealed class ReturnImpl : IObservable + { + private readonly T _value; + + public ReturnImpl(T value) + { + _value = value; + } + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + observer.OnCompleted(); + return Disposable.Empty; + } + } + + internal sealed class EmptyImpl : IObservable + { + internal static readonly IObservable Instance = new EmptyImpl(); + + private EmptyImpl() { } + + public IDisposable Subscribe(IObserver observer) + { + observer.OnCompleted(); + return Disposable.Empty; + } + } + + private sealed class CreateWithDisposableObservable : IObservable + { + private readonly Func, IDisposable> _subscribe; + + public CreateWithDisposableObservable(Func, IDisposable> subscribe) + { + _subscribe = subscribe; + } + + public IDisposable Subscribe(IObserver observer) + { + return _subscribe(observer); + } + } +} diff --git a/src/Avalonia.Base/Reactive/ObservableEx.cs b/src/Avalonia.Base/Reactive/ObservableEx.cs deleted file mode 100644 index 1cea568c88..0000000000 --- a/src/Avalonia.Base/Reactive/ObservableEx.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Reactive.Disposables; - -namespace Avalonia.Reactive -{ - /// - /// Provides common observable methods not found in standard Rx framework. - /// - public static class ObservableEx - { - /// - /// Returns an observable that fires once with the specified value and never completes. - /// - /// The type of the value. - /// The value. - /// The observable. - public static IObservable SingleValue(T value) - { - return new SingleValueImpl(value); - } - - private class SingleValueImpl : IObservable - { - private T _value; - - public SingleValueImpl(T value) - { - _value = value; - } - public IDisposable Subscribe(IObserver observer) - { - observer.OnNext(_value); - return Disposable.Empty; - } - } - } -} diff --git a/src/Avalonia.Base/Reactive/Operators/CombineLatest.cs b/src/Avalonia.Base/Reactive/Operators/CombineLatest.cs new file mode 100644 index 0000000000..aa95ceec35 --- /dev/null +++ b/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 : IObservable +{ + private readonly IObservable _first; + private readonly IObservable _second; + private readonly Func _resultSelector; + + public CombineLatest(IObservable first, IObservable second, + Func resultSelector) + { + _first = first; + _second = second; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) + { + var sink = new _(_resultSelector, observer); + sink.Run(_first, _second); + return sink; + } + + internal sealed class _ : IdentitySink + { + private readonly Func _resultSelector; + private readonly object _gate = new object(); + + public _(Func resultSelector, IObserver observer) + : base(observer) + { + _resultSelector = resultSelector; + _firstDisposable = null!; + _secondDisposable = null!; + } + + private IDisposable _firstDisposable; + private IDisposable _secondDisposable; + + public void Run(IObservable first, IObservable 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 + { + 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 + { + 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 : IObservable +{ + private readonly IEnumerable> _sources; + private readonly Func _resultSelector; + + public CombineLatest(IEnumerable> sources, Func resultSelector) + { + _sources = sources; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) + { + var sink = new _(_resultSelector, observer); + sink.Run(_sources); + return sink; + } + + internal sealed class _ : IdentitySink + { + private readonly object _gate = new object(); + private readonly Func _resultSelector; + + public _(Func resultSelector, IObserver 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> 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, 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(); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/Operators/Sink.cs b/src/Avalonia.Base/Reactive/Operators/Sink.cs new file mode 100644 index 0000000000..0fef350acc --- /dev/null +++ b/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 : IDisposable +{ + private IDisposable? _upstream; + private volatile IObserver _observer; + + protected Sink(IObserver observer) + { + _observer = observer; + } + + public void Dispose() + { + Dispose(true); + } + + /// + /// Override this method to dispose additional resources. + /// The method is guaranteed to be called at most once. + /// + /// If true, the method was called from . + 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.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 : Sink, IObserver +{ + protected Sink(IObserver observer) : base(observer) + { + } + + public virtual void Run(IObservable 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 GetForwarder() => new _(this); + + private sealed class _ : IObserver + { + private readonly Sink _forward; + + public _(Sink 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 : Sink +{ + protected IdentitySink(IObserver observer) : base(observer) + { + } + + public override void OnNext(T value) + { + ForwardOnNext(value); + } +} diff --git a/src/Avalonia.Base/Reactive/Operators/Switch.cs b/src/Avalonia.Base/Reactive/Operators/Switch.cs new file mode 100644 index 0000000000..bc849c499c --- /dev/null +++ b/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 : IObservable +{ + private readonly IObservable> _sources; + + public Switch(IObservable> sources) + { + _sources = sources; + } + + public IDisposable Subscribe(IObserver observer) + { + return _sources.Subscribe(new _(observer)); + } + + internal sealed class _ : Sink, TSource> + { + private readonly object _gate = new object(); + + public _(IObserver 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 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, 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(); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/SerialDisposableValue.cs b/src/Avalonia.Base/Reactive/SerialDisposableValue.cs new file mode 100644 index 0000000000..9eaf6343bf --- /dev/null +++ b/src/Avalonia.Base/Reactive/SerialDisposableValue.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive; + +/// +/// Represents a disposable resource whose underlying disposable resource can be replaced by another disposable resource, causing automatic disposal of the previous underlying disposable resource. +/// +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(); + } +} diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 198b36564a..261a39fa09 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs index 55893e9890..b9491e6cbd 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs index dd6cf7ad15..1bbf804b5f 100644 --- a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs +++ b/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 diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index ca602167c0..61cb31c6d0 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/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? _setters; private List? _animations; - private Subject? _animationTrigger; + private LightweightSubject? _animationTrigger; public StyleInstance( IStyle style, @@ -67,7 +67,7 @@ namespace Avalonia.Styling { if (_animations is not null && control is Animatable animatable) { - _animationTrigger ??= new Subject(); + _animationTrigger ??= new LightweightSubject(); foreach (var animation in _animations) animation.Apply(animatable, null, _animationTrigger); diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 8fc91a02d8..f81229eb48 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/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 diff --git a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs deleted file mode 100644 index 2a5b8d39c5..0000000000 --- a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - /// - /// Defines a listener to a event subscribed vis the . - /// - /// The type of the event arguments. - public interface IWeakSubscriber where T : EventArgs - { - /// - /// Invoked when the subscribed event is raised. - /// - /// The event sender. - /// The event arguments. - void OnEvent(object? sender, T e); - } -} diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs deleted file mode 100644 index e1c350d539..0000000000 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Reactive; -using System.Reactive.Linq; - -namespace Avalonia.Utilities -{ - /// - /// Provides extension methods for working with weak event handlers. - /// - public static class WeakObservable - { - - private class Handler - : IWeakSubscriber, - IWeakEventSubscriber where TEventArgs : EventArgs - { - private IObserver> _observer; - - public Handler(IObserver> observer) - { - _observer = observer; - } - - public void OnEvent(object? sender, TEventArgs e) - { - _observer.OnNext(new EventPattern(sender, e)); - } - - public void OnEvent(object? sender, WeakEvent ev, TEventArgs e) - { - _observer.OnNext(new EventPattern(sender, e)); - } - } - - /// - /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable - /// sequence, subscribing weakly. - /// - /// The type of target. - /// The type of the event args. - /// Object instance that exposes the event to convert. - /// The weak event to convert. - /// - public static IObservable> FromEventPattern( - TTarget target, WeakEvent ev) - where TEventArgs : EventArgs where TTarget : class - { - _ = target ?? throw new ArgumentNullException(nameof(target)); - _ = ev ?? throw new ArgumentNullException(nameof(ev)); - - return Observable.Create>(observer => - { - var handler = new Handler(observer); - ev.Subscribe(target, handler); - return () => ev.Unsubscribe(target, handler); - }).Publish().RefCount(); - } - - } -} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7e22303964..7fcb53bcea 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/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(params AvaloniaProperty[] properties) where T : Visual { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) + var invalidateObserver = new AnonymousObserver( + 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( + 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( - sender, static (target, _, _, _) => - { - target.InvalidateVisual(); - }); + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber( + 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 changes. /// /// The event args. - private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e) + private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs 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(); - 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(); diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 97f9efe8fa..5a31053bdc 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 2df46889a9..6f4c0003a8 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -15,6 +15,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index efa38e49a7..6556ce721e 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 81715e022b..f35124ee0a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -27,6 +27,7 @@ using Avalonia.Layout; using Avalonia.Controls.Metadata; using Avalonia.Input.GestureRecognizers; using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index ce74604f70..e859a6e725 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -4,12 +4,7 @@ // All other rights reserved. using Avalonia.Data; -using Avalonia.Utilities; using System; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Avalonia.Reactive; -using System.Diagnostics; using Avalonia.Controls.Utils; using Avalonia.Markup.Xaml.MarkupExtensions; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 641360dbe4..ea9b2fe972 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -15,6 +15,7 @@ using Avalonia.Utilities; using Avalonia.VisualTree; using System; using System.Diagnostics; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -1021,11 +1022,11 @@ namespace Avalonia.Controls { layoutableContent.LayoutUpdated += DetailsContent_LayoutUpdated; - _detailsContentSizeSubscription = - System.Reactive.Disposables.StableCompositeDisposable.Create( - System.Reactive.Disposables.Disposable.Create(() => layoutableContent.LayoutUpdated -= DetailsContent_LayoutUpdated), - _detailsContent.GetObservable(MarginProperty) - .Subscribe(DetailsContent_MarginChanged)); + _detailsContentSizeSubscription = new CompositeDisposable(2) + { + Disposable.Create(() => layoutableContent.LayoutUpdated -= DetailsContent_LayoutUpdated), + _detailsContent.GetObservable(MarginProperty).Subscribe(DetailsContent_MarginChanged) + }; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index c746b19cc7..10efded58a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -10,7 +10,7 @@ using Avalonia.Input; using Avalonia.Media; using System; using System.Diagnostics; -using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index 1d1a595ccf..d6c46bb1e0 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -2,7 +2,7 @@ using Avalonia.Reactive; using System; using System.Collections.Generic; -using System.Reactive.Subjects; +using Avalonia.Reactive; namespace Avalonia.Controls.Utils { @@ -16,16 +16,16 @@ namespace Avalonia.Controls.Utils internal class CellEditBinding : ICellEditBinding { - private readonly Subject _changedSubject = new Subject(); + private readonly LightweightSubject _changedSubject = new(); private readonly List _validationErrors = new List(); private readonly SubjectWrapper _inner; public bool IsValid => _validationErrors.Count <= 0; public IEnumerable ValidationErrors => _validationErrors; public IObservable ValidationChanged => _changedSubject; - public ISubject InternalSubject => _inner; + public IAvaloniaSubject InternalSubject => _inner; - public CellEditBinding(ISubject bindingSourceSubject) + public CellEditBinding(IAvaloniaSubject bindingSourceSubject) { _inner = new SubjectWrapper(bindingSourceSubject, this); } @@ -48,16 +48,16 @@ namespace Avalonia.Controls.Utils return IsValid; } - class SubjectWrapper : LightweightObservableBase, ISubject, IDisposable + class SubjectWrapper : LightweightObservableBase, IAvaloniaSubject, IDisposable { - private readonly ISubject _sourceSubject; + private readonly IAvaloniaSubject _sourceSubject; private readonly CellEditBinding _editBinding; private IDisposable _subscription; private object _controlValue; private bool _isControlValueSet = false; private bool _settingSourceValue = false; - public SubjectWrapper(ISubject bindingSourceSubject, CellEditBinding editBinding) + public SubjectWrapper(IAvaloniaSubject bindingSourceSubject, CellEditBinding editBinding) { _sourceSubject = bindingSourceSubject; _editBinding = editBinding; diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index dc9a0207ad..5b652cce19 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Concurrency; -using System.Threading; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -231,7 +229,6 @@ namespace Avalonia .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() - .Bind().ToConstant(AvaloniaScheduler.Instance) .Bind().ToConstant(DragDropDevice.Instance); // TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index f9978d2795..98885e11ca 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -10,7 +10,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index cf4beab6d4..42c577041a 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,7 +6,6 @@ - diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8e5d4e1e06..1ec6f8dabc 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -5,6 +5,7 @@ using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index e455c6c6f3..4b2997f431 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,7 +1,6 @@ -using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; +using Avalonia.Reactive; using Avalonia.Input; using Avalonia.Interactivity; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index ec1273ca98..b17648f5bb 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -7,7 +7,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index adee7d4d90..cd81c6e59b 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -1,7 +1,4 @@ -using System; -using System.Reactive.Concurrency; using Avalonia.Input; -using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index 6d7e542bb2..f5fcbed9fb 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Chrome [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { - private CompositeDisposable? _disposables; + private IDisposable? _disposables; /// /// Currently attached window. @@ -28,17 +28,14 @@ namespace Avalonia.Controls.Chrome { HostWindow = hostWindow; - _disposables = new CompositeDisposable - { - HostWindow.GetObservable(Window.WindowStateProperty) + _disposables = HostWindow.GetObservable(Window.WindowStateProperty) .Subscribe(x => { PseudoClasses.Set(":minimized", x == WindowState.Minimized); PseudoClasses.Set(":normal", x == WindowState.Normal); PseudoClasses.Set(":maximized", x == WindowState.Maximized); PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - }) - }; + }); } } diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 1bf13111a9..47b0bb6e2d 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Chrome if (VisualRoot is Window window) { - _disposables = new CompositeDisposable + _disposables = new CompositeDisposable(6) { window.GetObservable(Window.WindowDecorationMarginProperty) .Subscribe(x => UpdateSize(window)), diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 6b21870700..e3466d24ae 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Avalonia.Automation.Peers; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; diff --git a/src/Avalonia.Controls/ComboBoxItem.cs b/src/Avalonia.Controls/ComboBoxItem.cs index 83057d139f..7db713d692 100644 --- a/src/Avalonia.Controls/ComboBoxItem.cs +++ b/src/Avalonia.Controls/ComboBoxItem.cs @@ -1,5 +1,4 @@ -using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Automation; using Avalonia.Automation.Peers; @@ -12,8 +11,14 @@ namespace Avalonia.Controls { public ComboBoxItem() { - this.GetObservable(ComboBoxItem.IsFocusedProperty).Where(focused => focused) - .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this)); + this.GetObservable(ComboBoxItem.IsFocusedProperty) + .Subscribe(focused => + { + if (focused) + { + (Parent as ComboBox)?.ItemFocused(this); + } + }); } static ComboBoxItem() diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index c0b1297b6d..0e7d31967c 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -15,6 +15,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Styling; using Avalonia.Automation; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index bceeb58adb..889a4cc79f 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -1,8 +1,5 @@ using System; -using System.Linq; -using Avalonia.Data; -using Avalonia.LogicalTree; -using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index 00a06101c5..b082909807 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 64a02ccb46..5c35a09f1c 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls @@ -805,14 +806,12 @@ namespace Avalonia.Controls /// The properties. protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure(); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure()); foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + property.Changed.Subscribe(invalidateObserver); } } } diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 5d7b8998e6..f64cf79127 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents { var text = Text ?? ""; + if (string.IsNullOrEmpty(text)) + { + return; + } + var textRunProperties = CreateTextRunProperties(); var textCharacters = new TextCharacters(text, textRunProperties); diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 30bdb4c60e..e4487d29fa 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -3,6 +3,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using System; +using Avalonia.Reactive; using Avalonia.Media.Immutable; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 8455495830..9d4abec549 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -7,6 +7,7 @@ using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index dc3c621db4..de753f0bd0 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -2,6 +2,7 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 766a712c88..ce254684b7 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -4,7 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Media; namespace Avalonia.Controls @@ -424,9 +424,9 @@ namespace Avalonia.Controls if (newTransform != null) { - _transformChangedEvent = Observable.FromEventPattern( + _transformChangedEvent = Observable.FromEventPattern( v => newTransform.Changed += v, v => newTransform.Changed -= v) - .Subscribe(onNext: v => ApplyLayoutTransform()); + .Subscribe(_ => ApplyLayoutTransform()); } ApplyLayoutTransform(); diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 49e423aac8..aeee6f8410 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; @@ -159,12 +159,13 @@ namespace Avalonia.Controls // menu layout. var parentSharedSizeScope = this.GetObservable(VisualParentProperty) - .SelectMany(x => + .Select(x => { var parent = x as Control; return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? Observable.Return(null); - }); + }) + .Switch(); this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); } diff --git a/src/Avalonia.Controls/Mixins/DisposableMixin.cs b/src/Avalonia.Controls/Mixins/DisposableMixin.cs deleted file mode 100644 index 9b30b4ba4c..0000000000 --- a/src/Avalonia.Controls/Mixins/DisposableMixin.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Reactive.Disposables; - -namespace Avalonia.Controls.Mixins -{ - /// - /// Extension methods associated with the IDisposable interface. - /// - public static class DisposableMixin - { - /// - /// Ensures the provided disposable is disposed with the specified . - /// - /// - /// The type of the disposable. - /// - /// - /// The disposable we are going to want to be disposed by the CompositeDisposable. - /// - /// - /// The to which will be added. - /// - /// - /// The disposable. - /// - public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) - where T : IDisposable - { - if (compositeDisposable is null) - { - throw new ArgumentNullException(nameof(compositeDisposable)); - } - - compositeDisposable.Add(item); - return item; - } - } -} diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index a04a741e3e..2a8fe7b976 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -1,7 +1,7 @@ using System; using Avalonia.Interactivity; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Controls.Mixins { diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index fe650ab41e..c556ce7b02 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using Avalonia.Controls.Platform; -using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs index ac7a45a547..3953de8165 100644 --- a/src/Avalonia.Controls/NativeMenuBar.cs +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index d22fdb2f84..526eff0f12 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index a103adf185..663bd3358a 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -37,30 +37,29 @@ namespace Avalonia.Controls.Notifications RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); }); - // Disabling nullable checking because of https://github.com/dotnet/reactive/issues/1525 -#pragma warning disable CS8620 this.GetObservable(ContentProperty) - .OfType() -#pragma warning restore CS8620 .Subscribe(x => { - switch (x.Type) + if (x is Notification notification) { - case NotificationType.Error: - PseudoClasses.Add(":error"); - break; - - case NotificationType.Information: - PseudoClasses.Add(":information"); - break; - - case NotificationType.Success: - PseudoClasses.Add(":success"); - break; - - case NotificationType.Warning: - PseudoClasses.Add(":warning"); - break; + switch (notification.Type) + { + case NotificationType.Error: + PseudoClasses.Add(":error"); + break; + + case NotificationType.Information: + PseudoClasses.Add(":information"); + break; + + case NotificationType.Success: + PseudoClasses.Add(":success"); + break; + + case NotificationType.Warning: + PseudoClasses.Add(":warning"); + break; + } } }); } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 46c772f3b1..3ccddf4155 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 8ed6abd52b..ac4f699313 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -9,6 +9,7 @@ using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 7134e1b9e7..007d18c813 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -5,6 +5,7 @@ using System.Linq; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Reactive; using Avalonia.Styling; namespace Avalonia.Controls @@ -86,9 +87,11 @@ namespace Avalonia.Controls protected static void AffectsParentArrange(params AvaloniaProperty[] properties) where TPanel : Panel { + var invalidateObserver = new AnonymousObserver( + static e => AffectsParentArrangeInvalidate(e)); foreach (var property in properties) { - property.Changed.Subscribe(AffectsParentArrangeInvalidate); + property.Changed.Subscribe(invalidateObserver); } } @@ -99,9 +102,11 @@ namespace Avalonia.Controls protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) where TPanel : Panel { + var invalidateObserver = new AnonymousObserver( + static e => AffectsParentMeasureInvalidate(e)); foreach (var property in properties) { - property.Changed.Subscribe(AffectsParentMeasureInvalidate); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6d525da150..e09da02f17 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -6,9 +6,9 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls.Platform { diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 196fd898cf..bfd8bd73cb 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -1,7 +1,5 @@ -using System; -using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using System.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; @@ -17,7 +15,7 @@ namespace Avalonia.Platform private const RawInputModifiers MOUSE_INPUTMODIFIERS = RawInputModifiers.LeftMouseButton|RawInputModifiers.MiddleMouseButton|RawInputModifiers.RightMouseButton; private readonly IDragDropDevice _dragDrop; private readonly IInputManager _inputManager; - private readonly Subject _result = new Subject(); + private readonly LightweightSubject _result = new(); private DragDropEffects _allowedEffects; private IDataObject? _draggedData; @@ -44,11 +42,25 @@ namespace Avalonia.Platform _lastPosition = default; _allowedEffects = allowedEffects; - using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents)) + var inputObserver = new AnonymousObserver(arg => { - using (_inputManager.PreProcess.OfType().Subscribe(ProcessKeyEvents)) + switch (arg) { - var effect = await _result.FirstAsync(); + case RawPointerEventArgs pointerEventArgs: + ProcessMouseEvents(pointerEventArgs); + break; + case RawKeyEventArgs keyEventArgs: + ProcessKeyEvents(keyEventArgs); + break; + } + }); + + using (_inputManager.PreProcess.Subscribe(inputObserver)) + { + var tcs = new TaskCompletionSource(); + using (_result.Subscribe(new AnonymousObserver(tcs))) + { + var effect = await tcs.Task; return effect; } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 79ace0b329..6f043463d4 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Metadata; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c9bf135bbe..5a691ce665 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Utilities; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 7e5b34acd9..49e76d0728 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -1,7 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; using Avalonia.Automation.Peers; using Avalonia.Input; +using Avalonia.Reactive; using Avalonia.Media; using Avalonia.Media.TextFormatting; diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 89abe1cdaa..c9585d50ae 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index cd26ea4f6e..e0f72cae54 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 3c329a9a3e..bf79198939 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,6 +1,6 @@ using System; using System.ComponentModel; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Automation.Peers; using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 165bec3a95..7e0d695264 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs index d0b8178add..2c29b19d48 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index f2f735aaa9..8723304f8f 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,12 +1,12 @@ using System; using System.Numerics; -using System.Reactive.Linq; using Avalonia.Animation.Easings; using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Animations; diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 9dbc5f040e..68da24d79f 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls.Primitives; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 28d6911350..56e0cda8fe 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -4,16 +4,8 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading.Tasks; -using Avalonia.Controls.Presenters; -using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 12fb9ba5c5..503187e2d3 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index e2a13512a5..c8bf95b3f7 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Collections; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Reactive; namespace Avalonia.Controls.Shapes { diff --git a/src/Avalonia.Controls/Spinner.cs b/src/Avalonia.Controls/Spinner.cs index cfcf0ee376..bf6ed060bd 100644 --- a/src/Avalonia.Controls/Spinner.cs +++ b/src/Avalonia.Controls/Spinner.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index f39064435d..1d2bab8d27 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index fcadafaab5..35b135e152 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -6,6 +6,7 @@ using Avalonia.Media; using Avalonia.Metadata; using Avalonia.VisualTree; using System; +using Avalonia.Reactive; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index cfd4234a27..6fedf2b1cd 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Primitives; namespace Avalonia.Controls.Templates diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0e1024c027..f388dc871e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -2,7 +2,7 @@ using Avalonia.Input.Platform; using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index bb18bf4c64..d3339e3edf 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 2cd55dc3ab..1f5b8f80d2 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Notifications; using Avalonia.Controls.Platform; diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 41a1abd838..d1a7a1f727 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -6,7 +6,7 @@ using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Platform; -using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 0655af72a1..2fc901438d 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index d2e3feb55a..f8e06ba35d 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -1,8 +1,5 @@ using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using Avalonia.Reactive; namespace Avalonia.Controls.Utils { @@ -13,7 +10,7 @@ namespace Avalonia.Controls.Utils private readonly StyledElement _control; private readonly Type _ancestorType; public IObservable Observable => _subject; - private readonly Subject _subject = new Subject(); + private readonly LightweightSubject _subject = new(); private FinderNode? _child; private IDisposable? _disposable; @@ -31,24 +28,21 @@ namespace Avalonia.Controls.Utils private void OnValueChanged(StyledElement? next) { - if (next == null || _ancestorType.IsAssignableFrom(next.GetType())) + if (next == null || _ancestorType.IsInstanceOfType(next)) _subject.OnNext(next); else { _child?.Dispose(); _child = new FinderNode(next, _ancestorType); - _child.Observable.Subscribe(OnChildValueChanged); + _child.Observable.Subscribe(_subject); _child.Init(); } } - private void OnChildValueChanged(StyledElement? control) => _subject.OnNext(control); - - public void Dispose() { _child?.Dispose(); - _subject.Dispose(); + _subject.OnCompleted(); _disposable?.Dispose(); } } @@ -61,7 +55,7 @@ namespace Avalonia.Controls.Utils public static IObservable Create(StyledElement control, Type ancestorType) { - return new AnonymousObservable(observer => + return Observable.Create(observer => { var finder = new FinderNode(control, ancestorType); var subscription = finder.Observable.Subscribe(observer); diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 88f3b520ce..a20b4eee58 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; @@ -800,7 +800,7 @@ namespace Avalonia.Controls Renderer?.Start(); - Observable.FromEventPattern( + Observable.FromEventPattern( x => Closed += x, x => Closed -= x) .Take(1) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 65325aac92..aad0482b50 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,9 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 867c94b126..c5255b22cd 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,6 +19,5 @@ - diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 94679e8ade..3238b394fc 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls; diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index fe694b5730..65d1bea298 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -23,7 +23,6 @@ - diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 8eadcee6f9..e8cdcfa37b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Diagnostics { @@ -72,7 +71,11 @@ namespace Avalonia.Diagnostics { throw new ArgumentNullException(nameof(application)); } - var result = Disposable.Empty; + + var openedDisposable = new SerialDisposableValue(); + var result = new CompositeDisposable(2); + result.Add(openedDisposable); + // Skip if call on Design Mode if (!Avalonia.Controls.Design.IsDesignMode && !s_attachedToApplication) @@ -90,13 +93,15 @@ namespace Avalonia.Diagnostics { s_attachedToApplication = true; - application.InputManager.PreProcess.OfType().Subscribe(e => + result.Add(application.InputManager.PreProcess.Subscribe(e => { - if (options.Gesture.Matches(e)) + if (e is RawKeyEventArgs keyEventArgs + && keyEventArgs.Type == RawKeyEventType.KeyUp + && options.Gesture.Matches(keyEventArgs)) { - result = Open(application, options, owner); + openedDisposable.Disposable = Open(application, options, owner); } - }); + })); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 7da43a51b7..0140281d50 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -4,6 +4,7 @@ using Avalonia.Diagnostics.Views; using Avalonia.Interactivity; using Avalonia.Threading; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index e71f0bcaec..3048431af9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,10 +1,10 @@ using System; -using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; using System.Linq; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { @@ -84,7 +84,7 @@ namespace Avalonia.Diagnostics.ViewModels } nodes.Add(new LogicalTreeNode(window, Owner)); } - _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + _subscriptions = new CompositeDisposable(2) { Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 52535aa991..3870cad7c5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,11 +1,11 @@ using System; using System.ComponentModel; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { @@ -50,15 +50,15 @@ namespace Avalonia.Diagnostics.ViewModels } else { -#nullable disable - _pointerOverSubscription = InputManager.Instance.PreProcess - .OfType() + _pointerOverSubscription = InputManager.Instance!.PreProcess .Subscribe(e => { - PointerOverRoot = e.Root; - PointerOverElement = e.Root.InputHitTest(e.Position); + if (e is Input.Raw.RawPointerEventArgs pointerEventArgs) + { + PointerOverRoot = pointerEventArgs.Root; + PointerOverElement = pointerEventArgs.Root.InputHitTest(pointerEventArgs.Position); + } }); -#nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 65bfd7fc36..aafaa096e3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Linq; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { @@ -28,18 +28,8 @@ namespace Avalonia.Diagnostics.ViewModels { ElementName = control.Name; - var removed = Observable.FromEventPattern( - x => control.DetachedFromLogicalTree += x, - x => control.DetachedFromLogicalTree -= x); - var classesChanged = Observable.FromEventPattern< - NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventArgs>( - x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x) - .TakeUntil(removed); - - _classesSubscription = classesChanged.Select(_ => Unit.Default) - .StartWith(Unit.Default) + _classesSubscription = ((IObservable)control.Classes.GetWeakCollectionChangedObservable()) + .StartWith(null) .Subscribe(_ => { if (control.Classes.Count > 0) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index a1cd01c78b..f9fb0d18ef 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,12 +1,11 @@ using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; +using Avalonia.Reactive; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; using System.Linq; @@ -61,9 +60,12 @@ namespace Avalonia.Diagnostics.ViewModels IPopupHostProvider popupHostProvider, string? providerName = null) { - return Observable.FromEvent( - x => popupHostProvider.PopupHostChanged += x, - x => popupHostProvider.PopupHostChanged -= x) + return Observable.Create(observer => + { + void Handler(IPopupHost? args) => observer.OnNext(args); + popupHostProvider.PopupHostChanged += Handler; + return Disposable.Create(() => popupHostProvider.PopupHostChanged -= Handler); + }) .StartWith(popupHostProvider.PopupHost) .Select(popupHost => { @@ -80,29 +82,39 @@ namespace Avalonia.Diagnostics.ViewModels { Popup p => GetPopupHostObservable(p), Control c => Observable.CombineLatest( - c.GetObservable(Control.ContextFlyoutProperty), - c.GetObservable(Control.ContextMenuProperty), - c.GetObservable(FlyoutBase.AttachedFlyoutProperty), - c.GetObservable(ToolTipDiagnostics.ToolTipProperty), - c.GetObservable(Button.FlyoutProperty), - (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip, ButtonFlyout) => + new IObservable[] + { + c.GetObservable(Control.ContextFlyoutProperty), + c.GetObservable(Control.ContextMenuProperty), + c.GetObservable(FlyoutBase.AttachedFlyoutProperty), + c.GetObservable(ToolTipDiagnostics.ToolTipProperty), + c.GetObservable(Button.FlyoutProperty) + }) + .Select( + items => { - if (ContextMenu != null) + var contextFlyout = items[0]; + var contextMenu = (ContextMenu?)items[1]; + var attachedFlyout = items[2]; + var toolTip = items[3]; + var buttonFlyout = items[4]; + + if (contextMenu != null) //Note: ContextMenus are special since all the items are added as visual children. //So we don't need to go via Popup - return Observable.Return(new PopupRoot(ContextMenu)); + return Observable.Return(new PopupRoot(contextMenu)); - if (ContextFlyout != null) - return GetPopupHostObservable(ContextFlyout, "ContextFlyout"); + if (contextFlyout != null) + return GetPopupHostObservable(contextFlyout, "ContextFlyout"); - if (AttachedFlyout != null) - return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout"); + if (attachedFlyout != null) + return GetPopupHostObservable(attachedFlyout, "AttachedFlyout"); - if (ToolTip != null) - return GetPopupHostObservable(ToolTip, "ToolTip"); + if (toolTip != null) + return GetPopupHostObservable(toolTip, "ToolTip"); - if (ButtonFlyout != null) - return GetPopupHostObservable(ButtonFlyout, "Flyout"); + if (buttonFlyout != null) + return GetPopupHostObservable(buttonFlyout, "Flyout"); return Observable.Return(null); }) @@ -188,7 +200,7 @@ namespace Avalonia.Diagnostics.ViewModels } nodes.Add(new VisualTreeNode(window, Owner)); } - _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + _subscriptions = new CompositeDisposable(2) { Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs index c81e3cadf4..56d8737d79 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Diagnostics.Controls; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.Views { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 13b8cf5e8a..dbc4c98f78 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; @@ -13,13 +12,13 @@ using Avalonia.Markup.Xaml; using Avalonia.Styling; using Avalonia.Themes.Simple; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.Views { internal class MainWindow : Window, IStyleHost { - private readonly IDisposable? _keySubscription; - private readonly IDisposable? _pointerSubscription; + private readonly IDisposable? _inputSubscription; private readonly Dictionary _frozenPopupStates; private AvaloniaObject? _root; private PixelPoint _lastPointerPosition; @@ -33,15 +32,19 @@ namespace Avalonia.Diagnostics.Views if (Theme is null && this.FindResource(typeof(Window)) is ControlTheme windowTheme) Theme = windowTheme; - _keySubscription = InputManager.Instance?.Process - .OfType() - .Where(x => x.Type == RawKeyEventType.KeyDown) - .Subscribe(RawKeyDown); - _pointerSubscription = InputManager.Instance?.Process - .OfType() - .Subscribe(x => _lastPointerPosition = ((Visual)x.Root).PointToScreen(x.Position)); - - + _inputSubscription = InputManager.Instance?.Process + .Subscribe(x => + { + if (x is RawPointerEventArgs pointerEventArgs) + { + _lastPointerPosition = ((Visual)x.Root).PointToScreen(pointerEventArgs.Position); + } + else if (x is RawKeyEventArgs keyEventArgs && keyEventArgs.Type == RawKeyEventType.KeyDown) + { + RawKeyDown(keyEventArgs); + } + }); + _frozenPopupStates = new Dictionary(); EventHandler? lh = default; @@ -94,8 +97,7 @@ namespace Avalonia.Diagnostics.Views protected override void OnClosed(EventArgs e) { base.OnClosed(e); - _keySubscription?.Dispose(); - _pointerSubscription?.Dispose(); + _inputSubscription?.Dispose(); foreach (var state in _frozenPopupStates) { diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 3de79927b2..df67c06b73 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Specialized; using System.IO; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Runtime.InteropServices; using Avalonia.Collections; using Avalonia.Controls; @@ -120,10 +119,8 @@ namespace Avalonia.Dialogs.Internal .GetRequiredService() .Listen(ManagedFileChooserSources.MountedVolumes); - var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes, - nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged)) - .ObserveOn(AvaloniaScheduler.Instance) - .Subscribe(x => RefreshQuickLinks(quickSources)); + var sub2 = ManagedFileChooserSources.MountedVolumes.GetWeakCollectionChangedObservable() + .Subscribe(x => Dispatcher.UIThread.Post(() => RefreshQuickLinks(quickSources))); _disposables.Add(sub1); _disposables.Add(sub2); diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 791431dfa7..0f499c6066 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Reactive.Concurrency; using System.Reflection; using System.Threading.Tasks; using Avalonia.Input; diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 657e324010..560d6fc252 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index e37067d05c..b44762161b 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 34c1506a67..9f6b056d30 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -4,11 +4,12 @@ using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; using Avalonia.Controls.Platform; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; +using Avalonia.Reactive; using System.Text.RegularExpressions; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Threading; namespace Avalonia.FreeDesktop { @@ -17,21 +18,17 @@ namespace Avalonia.FreeDesktop private const string DevByLabelDir = "/dev/disk/by-label/"; private const string ProcPartitionsDir = "/proc/partitions"; private const string ProcMountsDir = "/proc/mounts"; - private CompositeDisposable _disposables; + private IDisposable _disposable; private ObservableCollection _targetObs; private bool _beenDisposed = false; public LinuxMountedVolumeInfoListener(ref ObservableCollection target) { - _disposables = new CompositeDisposable(); this._targetObs = target; - var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) - .Subscribe(Poll); + _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); - _disposables.Add(pollTimer); - - Poll(0); + Poll(); } private static string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); @@ -43,7 +40,7 @@ namespace Avalonia.FreeDesktop private static string UnescapeDeviceLabel(string input) => UnescapeString(input, @"\\x([0-9a-f]{2})", 16); - private void Poll(long _) + private bool Poll() { var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) .Skip(1) @@ -77,13 +74,14 @@ namespace Avalonia.FreeDesktop var mountVolInfos = q1.ToArray(); if (_targetObs.SequenceEqual(mountVolInfos)) - return; + return true; else { _targetObs.Clear(); foreach (var i in mountVolInfos) _targetObs.Add(i); + return true; } } @@ -93,7 +91,7 @@ namespace Avalonia.FreeDesktop { if (disposing) { - _disposables.Dispose(); + _disposable.Dispose(); _targetObs.Clear(); } diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index e7cba13ce0..301a23b608 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs index b233b46dd0..046e4645e3 100644 --- a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading; using Avalonia.Platform; using Avalonia.Threading; diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index e6a5a371d0..515835a3bd 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls; namespace Avalonia.Native.Interop diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index de3be6142e..424d2c7310 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Platform.Interop; diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs index 1907a3d129..6f45baeec2 100644 --- a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -2,15 +2,16 @@ using System; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; +using Avalonia.Reactive; using Avalonia.Controls.Platform; +using Avalonia.Threading; namespace Avalonia.Native { internal class MacOSMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; + private readonly IDisposable _disposable; private bool _beenDisposed = false; private ObservableCollection mountedDrives; @@ -18,17 +19,12 @@ namespace Avalonia.Native { this.mountedDrives = mountedDrives; - _disposables = new CompositeDisposable(); + _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); - var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) - .Subscribe(Poll); - - _disposables.Add(pollTimer); - - Poll(0); + Poll(); } - private void Poll(long _) + private bool Poll() { var mountVolInfos = Directory.GetDirectories("/Volumes/") .Where(p=> p != null) @@ -41,13 +37,14 @@ namespace Avalonia.Native .ToArray(); if (mountedDrives.SequenceEqual(mountVolInfos)) - return; + return true; else { mountedDrives.Clear(); foreach (var i in mountVolInfos) mountedDrives.Add(i); + return true; } } diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 4d75a776c3..0b4b7d26a1 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading; using Avalonia.Platform; +using Avalonia.Reactive; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index eea2587587..c67d7674a8 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Threading; +using Avalonia.Reactive; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs index fbfaf1bd3d..5573eb39fa 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Threading; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl; @@ -132,4 +131,4 @@ class EglConfigInfo SampleCount = sampleCount; StencilSize = stencilSize; } -} \ No newline at end of file +} diff --git a/src/Avalonia.ReactiveUI/Attributes.cs b/src/Avalonia.ReactiveUI/Attributes.cs index 31c374392f..1b85cd2f47 100644 --- a/src/Avalonia.ReactiveUI/Attributes.cs +++ b/src/Avalonia.ReactiveUI/Attributes.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: XmlnsDefinition("http://reactiveui.net", "Avalonia.ReactiveUI")] \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs index bd21503a58..ae9a8787bc 100644 --- a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -2,10 +2,10 @@ using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; using System.Threading; +using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Subjects; using System.Reactive.Linq; -using System.Reactive; using ReactiveUI; using System; using Avalonia.Controls.ApplicationLifetimes; diff --git a/src/Avalonia.ReactiveUI/AvaloniaObjectReactiveExtensions.cs b/src/Avalonia.ReactiveUI/AvaloniaObjectReactiveExtensions.cs new file mode 100644 index 0000000000..4cd329beb6 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AvaloniaObjectReactiveExtensions.cs @@ -0,0 +1,110 @@ +using System.Reactive; +using System.Reactive.Subjects; +using Avalonia.Data; + +namespace Avalonia.ReactiveUI; + +public static class AvaloniaObjectReactiveExtensions +{ + /// + /// Gets a subject for an . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); + } + + /// + /// Gets a subject for an . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } +} diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.ReactiveUI/AvaloniaScheduler.cs similarity index 98% rename from src/Avalonia.Base/Threading/AvaloniaScheduler.cs rename to src/Avalonia.ReactiveUI/AvaloniaScheduler.cs index 6423d86e7c..61c503e7a5 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaScheduler.cs @@ -1,8 +1,9 @@ using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; +using Avalonia.Threading; -namespace Avalonia.Threading +namespace Avalonia.ReactiveUI { /// /// A reactive scheduler that uses Avalonia's . diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index ab543703d9..fb52b3b147 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 1dd6426b39..4aa6b66743 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index def5228e94..88006f0b47 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; +using Avalonia.Reactive; namespace Avalonia.X11.Glx { class GlxContext : IGlContext diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 810a806c8a..5bbcf3af8c 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Avalonia.Reactive; using System.Text; using System.Threading.Tasks; using System.Threading; diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs index e56efdb4a8..4d3d8dcd97 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.Platform; using Avalonia.Skia; +using Avalonia.Reactive; namespace Avalonia.Browser.Skia { diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 267bd2520e..da739c754c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -66,7 +66,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs index 45e23db84f..170a31fd64 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data.Core; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs index 77ffa24687..cb21df0507 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; using System.Text; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index 81f1224650..abb166a92b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -5,6 +5,7 @@ using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs index 8489dd9d19..7c9f7559c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -1,10 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; -using System.Reactive.Subjects; using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { @@ -30,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings case TaskStatus.Faulted: return HandleCompleted(task); default: - var subject = new Subject(); + var subject = new LightweightSubject(); task.ContinueWith( x => HandleCompleted(task).Subscribe(subject), TaskScheduler.FromCurrentSynchronizationContext()) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 07c79d7077..23166170db 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,7 +7,6 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static ControlTemplateResult Load(object templateContent) - { if (templateContent is Func direct) { diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 753d577105..e3878b5bc6 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 6f836a799a..66907f33d0 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -1,15 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; using Avalonia.Controls; -using Avalonia.Data.Converters; using Avalonia.Data.Core; -using Avalonia.LogicalTree; using Avalonia.Markup.Parsers; -using Avalonia.Reactive; -using Avalonia.VisualTree; namespace Avalonia.Data { diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index c035f0b05d..90f312a249 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -1,9 +1,5 @@ - using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; @@ -247,6 +243,7 @@ namespace Avalonia.Data // Content property is bound to a value which becomes the ContentPresenter's // DataContext - it is from this that the child hosted by the ContentPresenter needs to // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) .Select(x => { @@ -255,7 +252,7 @@ namespace Avalonia.Data }).Switch(); } - private class UpdateSignal : SingleSubscriberObservableBase + private class UpdateSignal : SingleSubscriberObservableBase { private readonly AvaloniaObject _target; private readonly AvaloniaProperty _property; @@ -280,7 +277,7 @@ namespace Avalonia.Data { if (e.Property == _property) { - PublishNext(Unit.Default); + PublishNext(default); } } } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 4693b0c617..1515ff2c90 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Data.Converters; using Avalonia.Metadata; diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index 4dcdfb3c0e..4270063f87 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.Reactive.Subjects; using Avalonia.Data.Converters; using Avalonia.Reactive; using Avalonia.Styling; @@ -13,7 +12,7 @@ namespace Avalonia.Data public class TemplateBinding : SingleSubscriberObservableBase, IBinding, IDescription, - ISubject, + IAvaloniaSubject, ISetterValue { private bool _isSetterValue; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 6e6a163989..00f40dfcd3 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive; using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -64,7 +63,7 @@ namespace Avalonia.Markup.Parsers public static ExpressionObserver Build( Func rootGetter, string expression, - IObservable update, + IObservable update, bool enableDataValidation = false, string? description = null, Func? typeResolver = null) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index f9a3a61736..4fc17e440b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 697612ca12..ffbd34d492 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index c420a9df8d..ba1c1edffe 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -5,7 +5,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Reflection; using Avalonia.Data; using Avalonia.Data.Core; diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1ae47c8b7a..05fad25f1b 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 5bf1272c2f..4b3c7a016d 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index a7998353d9..e5fb182a3b 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 5fecaef100..32bcdba758 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 6153c7c75c..82fd1109f4 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Input; diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index d535efc5b7..b4002d8bc7 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 2b8bf6f8bf..fdf36206d0 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Platform; diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 585a9cdf19..e3d0cd9e5f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs index 32019f4c15..5be25a3d4b 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -1,8 +1,8 @@ using System; using System.Numerics; -using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL.Egl; +using Avalonia.Reactive; using Avalonia.Win32.Interop; using MicroCom.Runtime; diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index ba1bfda949..94e9c0c814 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -2,33 +2,30 @@ using System; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; +using Avalonia.Reactive; using Avalonia.Controls.Platform; using Avalonia.Logging; +using Avalonia.Threading; namespace Avalonia.Win32 { internal class WindowsMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; + private readonly IDisposable _disposable; private bool _beenDisposed = false; private ObservableCollection mountedDrives; public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) { this.mountedDrives = mountedDrives; - _disposables = new CompositeDisposable(); - var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) - .Subscribe(Poll); + _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); - _disposables.Add(pollTimer); - - Poll(0); + Poll(); } - private void Poll(long _) + private bool Poll() { var allDrives = DriveInfo.GetDrives(); @@ -56,13 +53,14 @@ namespace Avalonia.Win32 .ToArray(); if (mountedDrives.SequenceEqual(mountVolInfos)) - return; + return true; else { mountedDrives.Clear(); foreach (var i in mountVolInfos) mountedDrives.Add(i); + return true; } } @@ -72,7 +70,7 @@ namespace Avalonia.Win32 { if (disposing) { - + _disposable.Dispose(); } _beenDisposed = true; } diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 7a5e1a496d..8f7c1f3308 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Reactive; using OpenGLES; namespace Avalonia.iOS diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 547b2cb176..030d6ba215 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -1036,7 +1036,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public async Task Bind_With_Scheduler_Executes_On_Scheduler() + public async Task Bind_With_Scheduler_Executes_On_UI_Thread() { var target = new Class1(); var source = new Subject(); @@ -1047,7 +1047,6 @@ namespace Avalonia.Base.UnitTests .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - scheduler: AvaloniaScheduler.Instance, threadingInterface: threadingInterfaceMock.Object); using (UnitTestApplication.Start(services)) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs index 435980bd9a..612f3ff80a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs @@ -41,7 +41,7 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public void Should_Complete_When_Update_Observable_Completes() { - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; @@ -54,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public void Should_Complete_When_Update_Observable_Errors() { - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; @@ -87,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Unsubscribe_From_Update_Observable() { var scheduler = new TestScheduler(); - var update = scheduler.CreateColdObservable(); + var update = scheduler.CreateColdObservable(); var data = new { Foo = "foo" }; var target = ExpressionObserver.Create(() => data, o => o.Foo, update); var result = new List(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 144d9e6668..22d5645f76 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -359,14 +359,14 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Empty_Expression_Should_Track_Root() { var data = new Class1 { Foo = "foo" }; - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => data.Foo, o => o, update); var result = new List(); target.Subscribe(x => result.Add(x)); data.Foo = "bar"; - update.OnNext(Unit.Default); + update.OnNext(default); Assert.Equal(new[] { "foo", "bar" }, result); @@ -533,15 +533,15 @@ namespace Avalonia.Base.UnitTests.Data.Core var first = new Class1 { Foo = "foo" }; var second = new Class1 { Foo = "bar" }; var root = first; - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => root, o => o.Foo, update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); root = second; - update.OnNext(Unit.Default); + update.OnNext(default); root = null; - update.OnNext(Unit.Default); + update.OnNext(default); Assert.Equal( new object[] @@ -640,7 +640,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void RootGetter_Is_Reevaluated_On_Subscribe() { var data = "foo"; - var target = new ExpressionObserver(() => data, new EmptyExpressionNode(), new Subject(), null); + var target = new ExpressionObserver(() => data, new EmptyExpressionNode(), new Subject(), null); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs index e69eae43f0..9f7d9854fd 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs @@ -3,6 +3,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Reactive; using Avalonia.Styling.Activators; +using Observable = Avalonia.Reactive.Observable; namespace Avalonia.Base.UnitTests.Styling { @@ -10,12 +11,12 @@ namespace Avalonia.Base.UnitTests.Styling { public static IDisposable Subscribe(this IStyleActivator activator, Action action) { - return activator.ToObservable().Subscribe(action); + return Observable.Subscribe(activator.ToObservable(), action); } public static async Task Take(this IStyleActivator activator, int value) { - return await activator.ToObservable().Take(value); + return await System.Reactive.Linq.Observable.Take(activator.ToObservable(), value); } public static IObservable ToObservable(this IStyleActivator activator) diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 03d9332051..57338a1e08 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -15,4 +15,5 @@ + diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 53c186111d..656c2cbbbc 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -334,7 +334,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Subject; + var result = binding.Initiate(target, TextBox.TextProperty).Value; Assert.IsType(((BindingExpression)result).Converter); } @@ -350,7 +350,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Subject; + var result = binding.Initiate(target, TextBox.TextProperty).Value; Assert.Same(converter.Object, ((BindingExpression)result).Converter); } @@ -367,7 +367,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Bar", }; - var result = binding.Initiate(target, TextBox.TextProperty).Subject; + var result = binding.Initiate(target, TextBox.TextProperty).Value; Assert.Same("foo", ((BindingExpression)result).ConverterParameter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs index 44829aae1e..45deb97f51 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); - var subject = (BindingExpression)instanced.Subject; + var subject = (BindingExpression)instanced.Value; object result = null; subject.Subscribe(x => result = x); @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Subject; + var subject = (BindingExpression)instanced.Value; object result = null; subject.Subscribe(x => result = x); @@ -56,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template }; var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Subject; + var subject = (BindingExpression)instanced.Value; object result = null; subject.Subscribe(x => result = x); diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index cb5c625c35..a7ef2c4e4d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -84,6 +84,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("fallback", target.Text); } @@ -106,6 +107,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("(null)", target.Text); } @@ -128,6 +130,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("1,2,(unset)", target.Text); } @@ -150,6 +153,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("1,2,Fallback", target.Text); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetSubject.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaObjectTests_GetSubject.cs similarity index 95% rename from tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetSubject.cs rename to tests/Avalonia.ReactiveUI.UnitTests/AvaloniaObjectTests_GetSubject.cs index 0f2e8ebc21..81f04dc53e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetSubject.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaObjectTests_GetSubject.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Linq; using Xunit; -namespace Avalonia.Base.UnitTests +namespace Avalonia.ReactiveUI.UnitTests { public class AvaloniaObjectTests_GetSubject { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index aa499bb135..81d7b1854b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -1,6 +1,4 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 7fc27b01f4..1b6fd537eb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -62,6 +62,69 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + private class TextSourceWithDummyRuns : ITextSource + { + private readonly TextRunProperties _properties; + private readonly List> _textRuns; + + public TextSourceWithDummyRuns(TextRunProperties properties) + { + _properties = properties; + + _textRuns = new List> + { + new ValueSpan(0, 5, new TextCharacters("Hello", _properties)), + new ValueSpan(5, 1, new DummyRun()), + new ValueSpan(6, 1, new DummyRun()), + new ValueSpan(7, 6, new TextCharacters(" World", _properties)) + }; + } + + public TextRun GetTextRun(int textSourceIndex) + { + foreach (var run in _textRuns) + { + if (textSourceIndex < run.Start + run.Length) + { + return run.Value; + } + } + + return new TextEndOfParagraph(); + } + + private class DummyRun : TextRun + { + public DummyRun() + { + Length = DefaultTextSourceLength; + } + + public override int Length { get; } + } + } + + [Fact] + public void Should_Format_TextLine_With_Non_Text_TextRuns() + { + using (Start()) + { + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new TextSourceWithDummyRuns(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(5, textLine.TextRuns.Count); + + Assert.Equal(14, textLine.Length); + } + } + [Fact] public void Should_Format_TextRuns_With_TextRunStyles() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index ac2467407b..2c6ccfa896 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -326,7 +326,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); + var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)); + + Assert.Equal(currentDistance, actualDistance); } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 8f132433ec..40306a4513 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -71,7 +71,6 @@ namespace Avalonia.UnitTests IRuntimePlatform platform = null, IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, - IScheduler scheduler = null, ICursorFactory standardCursorFactory = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, @@ -91,7 +90,6 @@ namespace Avalonia.UnitTests RenderInterface = renderInterface; FontManagerImpl = fontManagerImpl; TextShaperImpl = textShaperImpl; - Scheduler = scheduler; StandardCursorFactory = standardCursorFactory; Theme = theme; ThreadingInterface = threadingInterface; @@ -110,7 +108,6 @@ namespace Avalonia.UnitTests public IPlatformRenderInterface RenderInterface { get; } public IFontManagerImpl FontManagerImpl { get; } public ITextShaperImpl TextShaperImpl { get; } - public IScheduler Scheduler { get; } public ICursorFactory StandardCursorFactory { get; } public Func Theme { get; } public IPlatformThreadingInterface ThreadingInterface { get; } @@ -149,7 +146,6 @@ namespace Avalonia.UnitTests renderInterface: renderInterface ?? RenderInterface, fontManagerImpl: fontManagerImpl ?? FontManagerImpl, textShaperImpl: textShaperImpl ?? TextShaperImpl, - scheduler: scheduler ?? Scheduler, standardCursorFactory: standardCursorFactory ?? StandardCursorFactory, theme: theme ?? Theme, threadingInterface: threadingInterface ?? ThreadingInterface, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 03e19359c3..82626877d0 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -66,7 +66,6 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.FontManagerImpl) .Bind().ToConstant(Services.TextShaperImpl) .Bind().ToConstant(Services.ThreadingInterface) - .Bind().ToConstant(Services.Scheduler) .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton();