Browse Source

Merge branch 'master' into refactor/itemcontainergenerator

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

46
NOTICE.md

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

1
build/Base.props

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

1
samples/ControlCatalog/Pages/MenuPage.xaml.cs

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

1
samples/ControlCatalog/Pages/PointerContactsTab.cs

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

1
samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs

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

1
samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

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

1
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

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

2
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;

2
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

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

3
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

1
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

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

1
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

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

4
samples/MiniMvvm/MiniMvvm.csproj

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

14
samples/MiniMvvm/PropertyChangedExtensions.cs

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

1
samples/MiniMvvm/ViewModelBase.cs

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

1
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

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

3
samples/Previewer/Previewer.csproj

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

1
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

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

2
samples/interop/WindowsInteropTest/WindowsInteropTest.csproj

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

2
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;

2
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;

3
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;

4
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
{

2
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);
}
}

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

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

2
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;

2
src/Avalonia.Base/Animation/Animators/TransformAnimator.cs

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

1
src/Avalonia.Base/Animation/Clock.cs

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

4
src/Avalonia.Base/Animation/CrossFade.cs

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

12
src/Avalonia.Base/Avalonia.Base.csproj

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

176
src/Avalonia.Base/AvaloniaObjectExtensions.cs

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

6
src/Avalonia.Base/AvaloniaProperty`1.cs

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

1
src/Avalonia.Base/ClassBindingManager.cs

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

2
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
{

1
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

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

2
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

11
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;

1
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

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

6
src/Avalonia.Base/Data/Core/BindingExpression.cs

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

17
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

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

61
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

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

65
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

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

5
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

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

2
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
{

9
src/Avalonia.Base/Data/IndexerBinding.cs

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

5
src/Avalonia.Base/Data/IndexerDescriptor.cs

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

50
src/Avalonia.Base/Data/InstancedBinding.cs

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

1
src/Avalonia.Base/Input/Gestures.cs

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

1
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

8
src/Avalonia.Base/Input/InputManager.cs

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

1
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;

2
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

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

3
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
{

6
src/Avalonia.Base/Interactivity/RoutedEvent.cs

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

18
src/Avalonia.Base/Layout/Layoutable.cs

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

9
src/Avalonia.Base/Media/Brush.cs

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

11
src/Avalonia.Base/Media/DashStyle.cs

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

9
src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs

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

4
src/Avalonia.Base/Media/Geometry.cs

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

1
src/Avalonia.Base/Media/GradientBrush.cs

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

1
src/Avalonia.Base/Media/MatrixTransform.cs

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

1
src/Avalonia.Base/Media/RotateTransform.cs

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

13
src/Avalonia.Base/Media/ScaleTransform.cs

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

13
src/Avalonia.Base/Media/SkewTransform.cs

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

11
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

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

2
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

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

23
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

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

218
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

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

7
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

133
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

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

4
src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs

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

106
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

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

4
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);
}
}
}

2
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

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

2
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

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

13
src/Avalonia.Base/Media/TranslateTransform.cs

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

2
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

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

62
src/Avalonia.Base/Reactive/AnonymousObserver.cs

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

23
src/Avalonia.Base/Reactive/CombinedSubject.cs

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

427
src/Avalonia.Base/Reactive/CompositeDisposable.cs

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

98
src/Avalonia.Base/Reactive/Disposable.cs

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

37
src/Avalonia.Base/Reactive/DisposableMixin.cs

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

8
src/Avalonia.Base/Reactive/IAvaloniaSubject.cs

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

8
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

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

30
src/Avalonia.Base/Reactive/LightweightSubject.cs

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

247
src/Avalonia.Base/Reactive/Observable.cs

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

37
src/Avalonia.Base/Reactive/ObservableEx.cs

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

374
src/Avalonia.Base/Reactive/Operators/CombineLatest.cs

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

111
src/Avalonia.Base/Reactive/Operators/Sink.cs

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

144
src/Avalonia.Base/Reactive/Operators/Switch.cs

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

35
src/Avalonia.Base/Reactive/SerialDisposableValue.cs

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

2
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;

2
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;

2
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

6
src/Avalonia.Base/Styling/StyleInstance.cs

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

2
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

18
src/Avalonia.Base/Utilities/IWeakSubscriber.cs

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

60
src/Avalonia.Base/Utilities/WeakObservable.cs

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

75
src/Avalonia.Base/Visual.cs

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

1
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save