From 581481f7adfe3d56899b04cee811b3d49756c0a0 Mon Sep 17 00:00:00 2001 From: zhouzj Date: Mon, 7 Nov 2022 09:52:06 +0800 Subject: [PATCH 01/78] Indicator size calculation should care about the ProgressBar's Padding property setting --- src/Avalonia.Controls/ProgressBar.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 71a7a58da4..165bec3a95 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -270,15 +270,16 @@ namespace Avalonia.Controls double percent = Maximum == Minimum ? 1.0 : (Value - Minimum) / (Maximum - Minimum); // When the Orientation changed, the indicator's Width or Height should set to double.NaN. + // Indicator size calculation should consider the ProgressBar's Padding property setting if (Orientation == Orientation.Horizontal) { - _indicator.Width = barSize.Width * percent; + _indicator.Width = (barSize.Width - _indicator.Margin.Left - _indicator.Margin.Right) * percent; _indicator.Height = double.NaN; } else { _indicator.Width = double.NaN; - _indicator.Height = barSize.Height * percent; + _indicator.Height = (barSize.Height - _indicator.Margin.Top - _indicator.Margin.Bottom) * percent; } From 5bdbd930d980999b249f3a7334b16fbd58b6e22e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 16 Nov 2022 16:56:11 +0000 Subject: [PATCH 02/78] Add Refresh Container --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/RefreshContainerPage.axaml | 24 + .../Pages/RefreshContainerPage.axaml.cs | 36 ++ .../ViewModels/RefreshContainerViewModel.cs | 26 + src/Avalonia.Base/Input/Gestures.cs | 8 + .../Input/PullGestureEventArgs.cs | 43 ++ .../Input/PullGestureRecognizer.cs | 152 +++++ .../PullToRefresh/RefreshContainer.cs | 221 +++++++ .../PullToRefresh/RefreshInfoProvider.cs | 129 +++++ .../PullToRefresh/RefreshVisualizer.cs | 544 ++++++++++++++++++ ...ScrollViewerIRefreshInfoProviderAdapter.cs | 227 ++++++++ .../Accents/FluentControlResourcesDark.xaml | 6 +- .../Accents/FluentControlResourcesLight.xaml | 4 + .../Controls/FluentControls.xaml | 2 + .../Controls/RefreshContainer.xaml | 24 + .../Controls/RefreshVisualizer.xaml | 67 +++ .../Accents/BaseDark.xaml | 3 + .../Accents/BaseLight.xaml | 4 +- .../Controls/RefreshContainer.xaml | 24 + .../Controls/RefreshVisualizer.xaml | 72 +++ .../Controls/SimpleControls.xaml | 2 + 21 files changed, 1619 insertions(+), 2 deletions(-) create mode 100644 samples/ControlCatalog/Pages/RefreshContainerPage.axaml create mode 100644 samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs create mode 100644 samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs create mode 100644 src/Avalonia.Base/Input/PullGestureEventArgs.cs create mode 100644 src/Avalonia.Base/Input/PullGestureRecognizer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b5a09b5fbd..33ea8c0db0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -132,6 +132,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml new file mode 100644 index 0000000000..0123251bd0 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs new file mode 100644 index 0000000000..4c52179e00 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class RefreshContainerPage : UserControl + { + private RefreshContainerViewModel _viewModel; + + public RefreshContainerPage() + { + this.InitializeComponent(); + + _viewModel = new RefreshContainerViewModel(); + + DataContext = _viewModel; + } + + private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var deferral = e.GetRefreshCompletionDeferral(); + + await _viewModel.AddToTop(); + + deferral.Complete(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs new file mode 100644 index 0000000000..a78bc9e44a --- /dev/null +++ b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using ControlCatalog.Pages; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class RefreshContainerViewModel : ViewModelBase + { + public ObservableCollection Items { get; } + + public RefreshContainerViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}")); + } + + public async Task AddToTop() + { + await Task.Delay(1000); + Items.Insert(0, $"Item {200 - Items.Count}"); + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 82ed96d982..acd78515cd 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -46,6 +46,14 @@ namespace Avalonia.Input private static readonly WeakReference s_lastPress = new WeakReference(null); private static Point s_lastPressPoint; + public static readonly RoutedEvent PullGestureEvent = + RoutedEvent.Register( + "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PullGestureEndedEvent = + RoutedEvent.Register( + "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); diff --git a/src/Avalonia.Base/Input/PullGestureEventArgs.cs b/src/Avalonia.Base/Input/PullGestureEventArgs.cs new file mode 100644 index 0000000000..57ad24f7a3 --- /dev/null +++ b/src/Avalonia.Base/Input/PullGestureEventArgs.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class PullGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + public PullDirection PullDirection { get; } + + private static int _nextId = 1; + + public static int GetNextFreeId() => _nextId++; + + public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) + { + Id = id; + Delta = delta; + PullDirection = pullDirection; + } + } + + public class PullGestureEndedEventArgs : RoutedEventArgs + { + public int Id { get; } + public PullDirection PullDirection { get; } + + public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent) + { + Id = id; + PullDirection = pullDirection; + } + } + + public enum PullDirection + { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft + } +} diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/PullGestureRecognizer.cs new file mode 100644 index 0000000000..bbbded44fa --- /dev/null +++ b/src/Avalonia.Base/Input/PullGestureRecognizer.cs @@ -0,0 +1,152 @@ +using Avalonia.Input.GestureRecognizers; + +namespace Avalonia.Input +{ + public class PullGestureRecognizer : StyledElement, IGestureRecognizer + { + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; + private Point _initialPosition; + private int _gestureId; + private IPointer? _tracking; + private PullDirection _pullDirection; + + /// + /// Defines the property. + /// + public static readonly DirectProperty PullDirectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(PullDirection), + o => o.PullDirection, + (o, v) => o.PullDirection = v); + + public PullDirection PullDirection + { + get => _pullDirection; + set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); + } + + public PullGestureRecognizer(PullDirection pullDirection) + { + PullDirection = pullDirection; + } + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + + _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPressed(e); + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + PointerReleased(e); + } + + public void PointerCaptureLost(IPointer pointer) + { + if (_tracking == pointer) + { + EndPull(); + } + } + + public void PointerMoved(PointerEventArgs e) + { + if (_tracking == e.Pointer) + { + var currentPosition = e.GetPosition(_target); + _actions!.Capture(e.Pointer, this); + + Vector delta = default; + switch (PullDirection) + { + case PullDirection.TopToBottom: + if (currentPosition.Y > _initialPosition.Y) + { + delta = new Vector(0, currentPosition.Y - _initialPosition.Y); + } + break; + case PullDirection.BottomToTop: + if (currentPosition.Y < _initialPosition.Y) + { + delta = new Vector(0, _initialPosition.Y - currentPosition.Y); + } + break; + case PullDirection.LeftToRight: + if (currentPosition.X > _initialPosition.X) + { + delta = new Vector(currentPosition.X - _initialPosition.X, 0); + } + break; + case PullDirection.RightToLeft: + if (currentPosition.X < _initialPosition.X) + { + delta = new Vector(_initialPosition.X - currentPosition.X, 0); + } + break; + } + + _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); + } + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + { + var position = e.GetPosition(_target); + + var canPull = false; + + var bounds = _target.Bounds; + + switch (PullDirection) + { + case PullDirection.TopToBottom: + canPull = position.Y < bounds.Height * 0.1; + break; + case PullDirection.BottomToTop: + canPull = position.Y > bounds.Height - (bounds.Height * 0.1); + break; + case PullDirection.LeftToRight: + canPull = position.X < bounds.Width * 0.1; + break; + case PullDirection.RightToLeft: + canPull = position.X > bounds.Width - (bounds.Width * 0.1); + break; + } + + if (canPull) + { + _gestureId = PullGestureEventArgs.GetNextFreeId(); + _tracking = e.Pointer; + _initialPosition = position; + } + } + } + + public void PointerReleased(PointerReleasedEventArgs e) + { + if (_tracking == e.Pointer) + { + EndPull(); + } + } + + private void EndPull() + { + _tracking = null; + _initialPosition = default; + + _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs new file mode 100644 index 0000000000..b882cf5a0f --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -0,0 +1,221 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public class RefreshContainer : ContentControl + { + internal const int DefaultPullDimensionSize = 100; + + private readonly bool _hasDefaultRefreshInfoProviderAdapter; + + private ScrollViewerIRefreshInfoProviderAdapter _refreshInfoProviderAdapter; + private RefreshInfoProvider _refreshInfoProvider; + private IDisposable _visualizerSizeSubscription; + private Grid? _visualizerPresenter; + private RefreshVisualizer _refreshVisualizer; + + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); + + public static readonly DirectProperty RefreshVisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), + s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); + + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + + public ScrollViewerIRefreshInfoProviderAdapter RefreshInfoProviderAdapter + { + get => _refreshInfoProviderAdapter; set + { + SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); + } + } + + private bool _hasDefaultRefreshVisualizer; + + public RefreshVisualizer RefreshVisualizer + { + get => _refreshVisualizer; set + { + if (_refreshVisualizer != null) + { + _visualizerSizeSubscription?.Dispose(); + _refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; + } + + SetAndRaise(RefreshVisualizerProperty, ref _refreshVisualizer, value); + } + } + + public PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); + } + + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshContainer() + { + _hasDefaultRefreshInfoProviderAdapter = true; + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _visualizerPresenter = e.NameScope.Find("PART_RefreshVisualizerPresenter"); + + if (_refreshVisualizer == null) + { + _hasDefaultRefreshVisualizer = true; + RefreshVisualizer = new RefreshVisualizer(); + } + else + { + _hasDefaultRefreshVisualizer = false; + RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); + } + + OnPullDirectionChanged(); + } + + private void OnVisualizerSizeChanged(Rect obj) + { + if (_hasDefaultRefreshInfoProviderAdapter) + { + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + } + + private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var ev = new RefreshRequestedEventArgs(e.GetRefreshCompletionDeferral(), RefreshRequestedEvent); + RaiseEvent(ev); + ev.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderAdapterProperty) + { + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + { + _refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + } + } + } + else if (change.Property == RefreshVisualizerProperty) + { + if (_visualizerPresenter != null) + { + _visualizerPresenter.Children.Clear(); + if (_refreshVisualizer != null) + { + _visualizerPresenter.Children.Add(_refreshVisualizer); + } + } + + if (_refreshVisualizer != null) + { + _refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested; + _visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged); + } + } + else if (change.Property == PullDirectionProperty) + { + OnPullDirectionChanged(); + } + } + + private void OnPullDirectionChanged() + { + if (_visualizerPresenter != null) + { + switch (PullDirection) + { + case PullDirection.TopToBottom: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.TopToBottom; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.BottomToTop: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.BottomToTop; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.LeftToRight: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.LeftToRight; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + case PullDirection.RightToLeft: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.RightToLeft; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + } + + if (_hasDefaultRefreshInfoProviderAdapter && + _hasDefaultRefreshVisualizer && + _refreshVisualizer.Bounds.Height == DefaultPullDimensionSize && + _refreshVisualizer.Bounds.Width == DefaultPullDimensionSize) + { + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + } + } + + public void RequestRefresh() + { + _refreshVisualizer?.RequestRefresh(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs new file mode 100644 index 0000000000..3fc32cdd08 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -0,0 +1,129 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls.PullToRefresh +{ + public class RefreshInfoProvider : Interactive + { + internal const double ExecutionRatio = 0.8; + + private readonly PullDirection _refreshPullDirection; + private readonly Size _refreshVisualizerSize; + + private readonly Visual _visual; + private bool _isInteractingForRefresh; + private double _interactionRatio; + private bool _entered; + + public DirectProperty IsInteractingForRefreshProperty = + AvaloniaProperty.RegisterDirect(nameof(IsInteractingForRefresh), + s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o); + + public DirectProperty InteractionRatioProperty = + AvaloniaProperty.RegisterDirect(nameof(InteractionRatio), + s => s.InteractionRatio, (s, o) => s.InteractionRatio = o); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshStartedEvent = + RoutedEvent.Register(nameof(RefreshStarted), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshCompletedEvent = + RoutedEvent.Register(nameof(RefreshCompleted), RoutingStrategies.Bubble); + + public bool PeekingMode { get; internal set; } + + public bool IsInteractingForRefresh + { + get => _isInteractingForRefresh; internal set + { + var isInteractingForRefresh = value && !PeekingMode; + + if (isInteractingForRefresh != _isInteractingForRefresh) + { + SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh); + } + } + } + + public double InteractionRatio + { + get => _interactionRatio; + set + { + SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value); + } + } + + internal Visual Visual => _visual; + + public event EventHandler RefreshStarted + { + add => AddHandler(RefreshStartedEvent, value); + remove => RemoveHandler(RefreshStartedEvent, value); + } + + public event EventHandler RefreshCompleted + { + add => AddHandler(RefreshCompletedEvent, value); + remove => RemoveHandler(RefreshCompletedEvent, value); + } + + internal void InteractingStateEntered(object sender, PullGestureEventArgs e) + { + if (!_entered) + { + IsInteractingForRefresh = true; + _entered = true; + } + + ValuesChanged(e.Delta); + } + + internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) + { + IsInteractingForRefresh = false; + _entered = false; + + ValuesChanged(default); + } + + + public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) + { + _refreshPullDirection = refreshPullDirection; + _refreshVisualizerSize = refreshVIsualizerSize; + _visual = visual; + } + + public void OnRefreshStarted() + { + RaiseEvent(new RoutedEventArgs(RefreshStartedEvent)); + } + + public void OnRefreshCompleted() + { + RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent)); + } + + internal void ValuesChanged(Vector value) + { + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height); + break; + case PullDirection.LeftToRight: + case PullDirection.RightToLeft: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs new file mode 100644 index 0000000000..81c613443d --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -0,0 +1,544 @@ +using System; +using System.Reactive.Linq; +using System.Threading; +using Avalonia.Animation; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public class RefreshVisualizer : ContentControl + { + private const int DefaultIndicatorSize = 24; + private const double MinimumIndicatorOpacity = 0.4; + private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; + private double _executingRatio = 0.8; + + private RotateTransform _visualizerRotateTransform; + private TranslateTransform _contentTranslateTransform; + private RefreshVisualizerState _refreshVisualizerState; + private RefreshInfoProvider _refreshInfoProvider; + private IDisposable _isInteractingSubscription; + private IDisposable _interactionRatioSubscription; + private bool _isInteractingForRefresh; + private Grid? _root; + private Control _content; + private RefreshVisualizerOrientation _orientation; + private float _startingRotationAngle; + private double _interactionRatio; + + private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop; + private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft; + + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + public static readonly DirectProperty RefreshVisualizerStateProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizerState), + s => s.RefreshVisualizerState); + + public static readonly DirectProperty OrientationProperty = + AvaloniaProperty.RegisterDirect(nameof(Orientation), + s => s.Orientation, (s, o) => s.Orientation = o); + + public DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + + public RefreshVisualizerState RefreshVisualizerState + { + get + { + return _refreshVisualizerState; + } + private set + { + SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value); + UpdateContent(); + } + } + + public RefreshVisualizerOrientation Orientation + { + get + { + return _orientation; + } + set + { + SetAndRaise(OrientationProperty, ref _orientation, value); + } + } + + internal PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set + { + SetValue(PullDirectionProperty, value); + + OnOrientationChanged(); + + UpdateContent(); + } + } + + public RefreshInfoProvider RefreshInfoProvider + { + get => _refreshInfoProvider; internal set + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.RenderTransform = null; + } + SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value); + } + } + + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshVisualizer() + { + _visualizerRotateTransform = new RotateTransform(); + _contentTranslateTransform = new TranslateTransform(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _root = e.NameScope.Find("PART_Root"); + + if (_root != null) + { + if (_content == null) + { + Content = new PathIcon() + { + Data = PathGeometry.Parse(ArrowPathData), + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize + }; + } + else + { + RaisePropertyChanged(ContentProperty, null, Content); + } + } + + OnOrientationChanged(); + + UpdateContent(); + } + + private void UpdateContent() + { + if (_content != null) + { + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _content.Classes.Remove("refreshing"); + _root.Classes.Remove("pending"); + _content.RenderTransform = _visualizerRotateTransform; + _content.Opacity = MinimumIndicatorOpacity; + _visualizerRotateTransform.Angle = _startingRotationAngle; + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = 0; + break; + case RefreshVisualizerState.Interacting: + _content.Classes.Remove("refreshing"); + _root.Classes.Remove("pending"); + _content.RenderTransform = _visualizerRotateTransform; + _content.Opacity = MinimumIndicatorOpacity; + _visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); + _content.Height = DefaultIndicatorSize; + _content.Width = DefaultIndicatorSize; + if (IsPullDirectionVertical) + { + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; + } + else + { + _contentTranslateTransform.Y = 0; + _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; + } + break; + case RefreshVisualizerState.Pending: + _content.Classes.Remove("refreshing"); + _content.Opacity = 1; + _content.RenderTransform = _visualizerRotateTransform; + if (IsPullDirectionVertical) + { + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; + } + else + { + _contentTranslateTransform.Y = 0; + _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; + } + + _root.Classes.Add("pending"); + break; + case RefreshVisualizerState.Refreshing: + _root.Classes.Remove("pending"); + _content.Classes.Add("refreshing"); + _content.Opacity = 1; + _content.Height = DefaultIndicatorSize; + _content.Width = DefaultIndicatorSize; + break; + case RefreshVisualizerState.Peeking: + _root.Classes.Remove("pending"); + _content.Opacity = 1; + _visualizerRotateTransform.Angle += _startingRotationAngle; + break; + } + } + } + + public void RequestRefresh() + { + RefreshVisualizerState = RefreshVisualizerState.Refreshing; + RefreshInfoProvider?.OnRefreshStarted(); + + RaiseRefreshRequested(); + } + + private void RefreshCompleted() + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + + RefreshInfoProvider?.OnRefreshCompleted(); + } + + private void RaiseRefreshRequested() + { + var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent); + + refreshArgs.IncrementCount(); + + RaiseEvent(refreshArgs); + + refreshArgs.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderProperty) + { + OnRefreshInfoProviderChanged(); + } + else if (change.Property == ContentProperty) + { + if (_root != null) + { + if (_content == null) + { + _content = new PathIcon() + { + Data = PathGeometry.Parse(ArrowPathData), + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize + }; + + var transformGroup = new TransformGroup(); + transformGroup.Children.Add(_visualizerRotateTransform); + + _content.RenderTransform = _visualizerRotateTransform; + _root.RenderTransform = _contentTranslateTransform; + + var transition = new Transitions + { + new DoubleTransition() + { + Property = OpacityProperty, + Duration = TimeSpan.FromSeconds(0.5) + }, + }; + + _content.Transitions = transition; + } + + var scalingGrid = new Grid(); + scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; + scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; + scalingGrid.RenderTransform = new ScaleTransform(); + + scalingGrid.Children.Add(_content); + + _root.Children.Insert(0, scalingGrid); + _content.VerticalAlignment = Layout.VerticalAlignment.Center; + _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; + } + + UpdateContent(); + } + else if (change.Property == OrientationProperty) + { + OnOrientationChanged(); + + UpdateContent(); + } + else if (change.Property == BoundsProperty) + { + if (_content != null) + { + var parent = _content.Parent as Control; + switch (PullDirection) + { + case PullDirection.TopToBottom: + parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); + break; + case PullDirection.BottomToTop: + parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); + break; + case PullDirection.LeftToRight: + parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); + break; + case PullDirection.RightToLeft: + parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0); + break; + } + } + } + } + + private void OnOrientationChanged() + { + switch (_orientation) + { + case RefreshVisualizerOrientation.Auto: + switch (PullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + _startingRotationAngle = 0.0f; + break; + case PullDirection.LeftToRight: + _startingRotationAngle = 270; + break; + case PullDirection.RightToLeft: + _startingRotationAngle = 90; + break; + } + break; + case RefreshVisualizerOrientation.Normal: + _startingRotationAngle = 0.0f; + break; + case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise: + _startingRotationAngle = 270; + break; + case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise: + _startingRotationAngle = 90; + break; + } + } + + private void OnRefreshInfoProviderChanged() + { + _isInteractingSubscription?.Dispose(); + _isInteractingSubscription = null; + _interactionRatioSubscription?.Dispose(); + _interactionRatioSubscription = null; + + if (_refreshInfoProvider != null) + { + _isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + .Subscribe(InteractingForRefreshObserver); + + _interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + .Subscribe(InteractionRatioObserver); + + var visual = _refreshInfoProvider.Visual; + visual.RenderTransform = _contentTranslateTransform; + + _executingRatio = RefreshInfoProvider.ExecutionRatio; + } + else + { + _executingRatio = 1; + } + } + + private void InteractionRatioObserver(double obj) + { + var wasAtZero = _interactionRatio == 0.0; + _interactionRatio = obj; + + if (_isInteractingForRefresh) + { + if (RefreshVisualizerState == RefreshVisualizerState.Idle) + { + if (wasAtZero) + { + if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Interacting) + { + if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else + { + UpdateContent(); + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Pending) + { + if (_interactionRatio <= _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + else if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else + { + UpdateContent(); + } + } + } + else + { + if (RefreshVisualizerState != RefreshVisualizerState.Refreshing) + { + if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + else + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + } + } + } + + private void InteractingForRefreshObserver(bool obj) + { + _isInteractingForRefresh = obj; + + if (!_isInteractingForRefresh) + { + switch (_refreshVisualizerState) + { + case RefreshVisualizerState.Pending: + RequestRefresh(); + break; + case RefreshVisualizerState.Refreshing: + // We don't want to interrupt a currently executing refresh. + break; + default: + RefreshVisualizerState = RefreshVisualizerState.Idle; + break; + } + } + } + } + + public enum RefreshVisualizerState + { + Idle, + Peeking, + Interacting, + Pending, + Refreshing + } + + public enum RefreshVisualizerOrientation + { + Auto, + Normal, + Rotate90DegreesCounterclockwise, + Rotate270DegreesCounterclockwise + } + + public class RefreshRequestedEventArgs : RoutedEventArgs + { + private RefreshCompletionDeferral _refreshCompletionDeferral; + + public RefreshCompletionDeferral GetRefreshCompletionDeferral() + { + return _refreshCompletionDeferral.Get(); + } + + public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); + } + + public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = completionDeferral; + } + + internal void IncrementCount() + { + _refreshCompletionDeferral?.Get(); + } + + internal void DecrementCount() + { + _refreshCompletionDeferral?.Complete(); + } + } + + public class RefreshCompletionDeferral + { + private Action _deferredAction; + private int _deferCount; + + public RefreshCompletionDeferral(Action deferredAction) + { + _deferredAction = deferredAction; + } + + public void Complete() + { + Interlocked.Decrement(ref _deferCount); + + if (_deferCount == 0) + { + _deferredAction?.Invoke(); + } + } + + public RefreshCompletionDeferral Get() + { + Interlocked.Increment(ref _deferCount); + + return this; + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs new file mode 100644 index 0000000000..0b0c8c99b2 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -0,0 +1,227 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.PullToRefresh +{ + public class ScrollViewerIRefreshInfoProviderAdapter + { + private const int MaxSearchDepth = 10; + private const int InitialOffsetThreshold = 1; + + private PullDirection _refreshPullDirection; + private ScrollViewer _scrollViewer; + private RefreshInfoProvider _refreshInfoProvider; + private PullGestureRecognizer _pullGestureRecognizer; + private InputElement? _interactionSource; + private bool _isVisualizerInteractionSourceAttached; + + public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection) + { + _refreshPullDirection = pullDirection; + } + + public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) + { + if (root is ScrollViewer scrollViewer) + { + return Adapt(scrollViewer, refreshVIsualizerSize); + } + else + { + int depth = 0; + while (depth < MaxSearchDepth) + { + var scroll = AdaptFromTreeRecursiveHelper(root, depth); + + if (scroll != null) + { + return Adapt(scroll, refreshVIsualizerSize); + } + + depth++; + } + } + + ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) + { + if (depth == 0) + { + foreach (var child in root.VisualChildren) + { + if (child is ScrollViewer viewer) + { + return viewer; + } + } + } + else + { + foreach (var child in root.VisualChildren) + { + var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1); + if (viewer != null) + { + return viewer; + } + } + } + + return null; + } + + return null; + } + + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) + { + if (adaptee == null) + { + throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null"); + } + + if (_scrollViewer != null) + { + CleanUpScrollViewer(); + } + + if (_refreshInfoProvider != null && _interactionSource != null) + { + _interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + } + + _refreshInfoProvider = null; + _scrollViewer = adaptee; + + if (_scrollViewer.Content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null."); + } + + var content = adaptee.Content as Visual; + + if (content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual"); + } + + if (content.GetVisualParent() == null) + { + _scrollViewer.Loaded += ScrollViewer_Loaded; + } + else + { + ScrollViewer_Loaded(null, null); + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement"); + } + } + + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); + + _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); + + if (_interactionSource != null) + { + _interactionSource.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + + _scrollViewer.PointerPressed += ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased += ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; + + return _refreshInfoProvider; + } + + private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) + { + if (!IsWithinOffsetThreashold()) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + } + + private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + { + var content = _scrollViewer.Content as Visual; + if (content == null) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); + } + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement"); + } + + MakeInteractionSource(content.Parent as InputElement); + + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } + + private void MakeInteractionSource(InputElement element) + { + _interactionSource = element; + + if (_pullGestureRecognizer != null) + { + element.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + } + + private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + + private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } + + private bool IsWithinOffsetThreashold() + { + if (_scrollViewer != null) + { + var offset = _scrollViewer.Offset; + + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + return offset.Y < InitialOffsetThreshold; + case PullDirection.LeftToRight: + return offset.X < InitialOffsetThreshold; + case PullDirection.RightToLeft: + return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold; + case PullDirection.BottomToTop: + return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold; + } + } + + return false; + } + + private void CleanUpScrollViewer() + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 7e3c8673f5..d9be545801 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -637,6 +637,10 @@ - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 7917315e19..e80b137b2d 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -633,5 +633,9 @@ + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5383aa3180..5d38b055b3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -69,6 +69,8 @@ + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..97002c25bd --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..388ca814f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml index 1843abebfd..c748775a12 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml @@ -33,6 +33,9 @@ + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml index 6247815303..43acb16be5 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml @@ -33,6 +33,8 @@ - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..326b217068 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..0b12fe133d --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4aefa0136c..4bad556338 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -65,6 +65,8 @@ + + From 68fd64f1640b657eb3f15987ff9e077eb7f366ac Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 17 Nov 2022 15:29:39 +0000 Subject: [PATCH 03/78] Use Composition Api for animation --- .../Pages/RefreshContainerPage.axaml | 2 + .../Input/PullGestureRecognizer.cs | 2 +- .../PullToRefresh/RefreshContainer.cs | 48 +-- .../PullToRefresh/RefreshInfoProvider.cs | 13 +- .../PullToRefresh/RefreshVisualizer.cs | 282 +++++++++++------- ...ScrollViewerIRefreshInfoProviderAdapter.cs | 91 ++++-- .../Controls/RefreshVisualizer.xaml | 32 +- 7 files changed, 277 insertions(+), 193 deletions(-) diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml index 0123251bd0..d467d14427 100644 --- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -2,9 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="using:ControlCatalog.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:DataType="viewModels:RefreshContainerViewModel" x:Class="ControlCatalog.Pages.RefreshContainerPage"> RefreshRequestedEvent = RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); - internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); - public static readonly DirectProperty RefreshVisualizerProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), + public static readonly DirectProperty RefreshVisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); public static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); - public ScrollViewerIRefreshInfoProviderAdapter RefreshInfoProviderAdapter + public ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter { get => _refreshInfoProviderAdapter; set { @@ -42,7 +42,7 @@ namespace Avalonia.Controls private bool _hasDefaultRefreshVisualizer; - public RefreshVisualizer RefreshVisualizer + public RefreshVisualizer? RefreshVisualizer { get => _refreshVisualizer; set { @@ -88,7 +88,7 @@ namespace Avalonia.Controls else { _hasDefaultRefreshVisualizer = false; - RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); + RaisePropertyChanged(RefreshVisualizerProperty, default, _refreshVisualizer); } OnPullDirectionChanged(); @@ -115,19 +115,23 @@ namespace Avalonia.Controls if (change.Property == RefreshInfoProviderAdapterProperty) { - if (_refreshInfoProvider != null) - { - _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; - } - else + if (_refreshVisualizer != null) { - if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + if (_refreshInfoProvider != null) { - _refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); - - if (_refreshInfoProvider != null) + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) { - _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + _refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer); + } } } } @@ -157,7 +161,7 @@ namespace Avalonia.Controls private void OnPullDirectionChanged() { - if (_visualizerPresenter != null) + if (_visualizerPresenter != null && _refreshVisualizer != null) { switch (PullDirection) { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs index 3fc32cdd08..506b48f6e2 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; namespace Avalonia.Controls.PullToRefresh { @@ -11,7 +12,7 @@ namespace Avalonia.Controls.PullToRefresh private readonly PullDirection _refreshPullDirection; private readonly Size _refreshVisualizerSize; - private readonly Visual _visual; + private readonly CompositionVisual? _visual; private bool _isInteractingForRefresh; private double _interactionRatio; private bool _entered; @@ -60,7 +61,7 @@ namespace Avalonia.Controls.PullToRefresh } } - internal Visual Visual => _visual; + internal CompositionVisual? Visual => _visual; public event EventHandler RefreshStarted { @@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh remove => RemoveHandler(RefreshCompletedEvent, value); } - internal void InteractingStateEntered(object sender, PullGestureEventArgs e) + internal void InteractingStateEntered(object? sender, PullGestureEventArgs e) { if (!_entered) { @@ -85,7 +86,7 @@ namespace Avalonia.Controls.PullToRefresh ValuesChanged(e.Delta); } - internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) + internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e) { IsInteractingForRefresh = false; _entered = false; @@ -94,10 +95,10 @@ namespace Avalonia.Controls.PullToRefresh } - public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) + public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual) { _refreshPullDirection = refreshPullDirection; - _refreshVisualizerSize = refreshVIsualizerSize; + _refreshVisualizerSize = refreshVIsualizerSize ?? default; _visual = visual; } diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 81c613443d..5d0eee7478 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using System.Reactive.Linq; using System.Threading; using Avalonia.Animation; @@ -7,25 +8,24 @@ using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Rendering.Composition; namespace Avalonia.Controls { public class RefreshVisualizer : ContentControl { private const int DefaultIndicatorSize = 24; - private const double MinimumIndicatorOpacity = 0.4; + private const float MinimumIndicatorOpacity = 0.4f; private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; private double _executingRatio = 0.8; - private RotateTransform _visualizerRotateTransform; - private TranslateTransform _contentTranslateTransform; private RefreshVisualizerState _refreshVisualizerState; - private RefreshInfoProvider _refreshInfoProvider; - private IDisposable _isInteractingSubscription; - private IDisposable _interactionRatioSubscription; + private RefreshInfoProvider? _refreshInfoProvider; + private IDisposable? _isInteractingSubscription; + private IDisposable? _interactionRatioSubscription; private bool _isInteractingForRefresh; private Grid? _root; - private Control _content; + private Control? _content; private RefreshVisualizerOrientation _orientation; private float _startingRotationAngle; private double _interactionRatio; @@ -46,9 +46,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(Orientation), s => s.Orientation, (s, o) => s.Orientation = o); - public DirectProperty RefreshInfoProviderProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + public DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + private Vector3 _defaultOffset; + private bool _played; public RefreshVisualizerState RefreshVisualizerState { @@ -88,7 +90,7 @@ namespace Avalonia.Controls } } - public RefreshInfoProvider RefreshInfoProvider + public RefreshInfoProvider? RefreshInfoProvider { get => _refreshInfoProvider; internal set { @@ -106,20 +108,17 @@ namespace Avalonia.Controls remove => RemoveHandler(RefreshRequestedEvent, value); } - public RefreshVisualizer() - { - _visualizerRotateTransform = new RotateTransform(); - _contentTranslateTransform = new TranslateTransform(); - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); + this.ClipToBounds = false; + _root = e.NameScope.Find("PART_Root"); if (_root != null) { + _content = Content as Control; if (_content == null) { Content = new PathIcon() @@ -140,69 +139,113 @@ namespace Avalonia.Controls UpdateContent(); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + UpdateContent(); + } + private void UpdateContent() { - if (_content != null) + if (_content != null && _root != null) { - switch (RefreshVisualizerState) + var root = _root; + var visual = _refreshInfoProvider?.Visual; + var contentVisual = ElementComposition.GetElementVisual(_content); + var visualizerVisual = ElementComposition.GetElementVisual(this); + if (visual != null && contentVisual != null && visualizerVisual != null) { - case RefreshVisualizerState.Idle: - _content.Classes.Remove("refreshing"); - _root.Classes.Remove("pending"); - _content.RenderTransform = _visualizerRotateTransform; - _content.Opacity = MinimumIndicatorOpacity; - _visualizerRotateTransform.Angle = _startingRotationAngle; - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = 0; - break; - case RefreshVisualizerState.Interacting: - _content.Classes.Remove("refreshing"); - _root.Classes.Remove("pending"); - _content.RenderTransform = _visualizerRotateTransform; - _content.Opacity = MinimumIndicatorOpacity; - _visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); - _content.Height = DefaultIndicatorSize; - _content.Width = DefaultIndicatorSize; - if (IsPullDirectionVertical) - { - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; - } - else - { - _contentTranslateTransform.Y = 0; - _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; - } - break; - case RefreshVisualizerState.Pending: - _content.Classes.Remove("refreshing"); - _content.Opacity = 1; - _content.RenderTransform = _visualizerRotateTransform; - if (IsPullDirectionVertical) - { - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; - } - else - { - _contentTranslateTransform.Y = 0; - _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; - } - - _root.Classes.Add("pending"); - break; - case RefreshVisualizerState.Refreshing: - _root.Classes.Remove("pending"); - _content.Classes.Add("refreshing"); - _content.Opacity = 1; - _content.Height = DefaultIndicatorSize; - _content.Width = DefaultIndicatorSize; - break; - case RefreshVisualizerState.Peeking: - _root.Classes.Remove("pending"); - _content.Opacity = 1; - _visualizerRotateTransform.Angle += _startingRotationAngle; - break; + contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0); + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, 0, 0) : + new Vector3(0, visualizerVisual.Offset.Y, 0); + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + visual.Offset = default; + break; + case RefreshVisualizerState.Interacting: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)((_startingRotationAngle + (_interactionRatio * 360)) * Math.PI / 180f); + Vector3 offset = default; + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + break; + case RefreshVisualizerState.Pending: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = (float)((_startingRotationAngle + 360) * Math.PI / 180f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + + if (!_played) + { + _played = true; + var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1)); + scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1)); + scaleAnimation.Duration = TimeSpan.FromSeconds(0.3); + + contentVisual.StartAnimation("Scale", scaleAnimation); + } + break; + case RefreshVisualizerState.Refreshing: + var rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation(); + rotateAnimation.Target = "RotationAngle"; + rotateAnimation.InsertKeyFrame(0, (float)(0)); + rotateAnimation.InsertKeyFrame(0.5f, (float)(Math.PI)); + rotateAnimation.InsertKeyFrame(1, (float)(2 * Math.PI)); + rotateAnimation.Duration = TimeSpan.FromSeconds(1); + rotateAnimation.IterationCount = 1000; + + contentVisual.StartAnimation("RotationAngle", rotateAnimation); + contentVisual.Opacity = 1; + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + break; + case RefreshVisualizerState.Peeking: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); + break; + } } } } @@ -254,12 +297,6 @@ namespace Avalonia.Controls Width = DefaultIndicatorSize }; - var transformGroup = new TransformGroup(); - transformGroup.Children.Add(_visualizerRotateTransform); - - _content.RenderTransform = _visualizerRotateTransform; - _root.RenderTransform = _contentTranslateTransform; - var transition = new Transitions { new DoubleTransition() @@ -270,16 +307,42 @@ namespace Avalonia.Controls }; _content.Transitions = transition; - } - var scalingGrid = new Grid(); - scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; - scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; - scalingGrid.RenderTransform = new ScaleTransform(); - - scalingGrid.Children.Add(_content); + _content.Loaded += (s, e) => + { + var composition = ElementComposition.GetElementVisual(_content); + var compositor = composition!.Compositor; + composition.Opacity = 0; + + var smoothRotationAnimation + = compositor.CreateScalarKeyFrameAnimation(); + smoothRotationAnimation.Target = "RotationAngle"; + smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var scaleAnimation + = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["RotationAngle"] = smoothRotationAnimation; + animation["Offset"] = offsetAnimation; + animation["Scale"] = scaleAnimation; + + composition.ImplicitAnimations = animation; + + UpdateContent(); + }; + } - _root.Children.Insert(0, scalingGrid); + _root.Children.Add(_content); _content.VerticalAlignment = Layout.VerticalAlignment.Center; _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; } @@ -294,25 +357,23 @@ namespace Avalonia.Controls } else if (change.Property == BoundsProperty) { - if (_content != null) + switch (PullDirection) { - var parent = _content.Parent as Control; - switch (PullDirection) - { - case PullDirection.TopToBottom: - parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); - break; - case PullDirection.BottomToTop: - parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); - break; - case PullDirection.LeftToRight: - parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); - break; - case PullDirection.RightToLeft: - parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0); - break; - } + case PullDirection.TopToBottom: + RenderTransform = new TranslateTransform(0, -Bounds.Height); + break; + case PullDirection.BottomToTop: + RenderTransform = new TranslateTransform(0, Bounds.Height); + break; + case PullDirection.LeftToRight: + RenderTransform = new TranslateTransform(-Bounds.Width, 0); + break; + case PullDirection.RightToLeft: + RenderTransform = new TranslateTransform(Bounds.Width, 0); + break; } + + UpdateContent(); } } @@ -354,16 +415,15 @@ namespace Avalonia.Controls _interactionRatioSubscription?.Dispose(); _interactionRatioSubscription = null; - if (_refreshInfoProvider != null) + if (RefreshInfoProvider != null) { - _isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + _isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) .Subscribe(InteractingForRefreshObserver); - _interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + _interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) .Subscribe(InteractionRatioObserver); - var visual = _refreshInfoProvider.Visual; - visual.RenderTransform = _contentTranslateTransform; + var visual = RefreshInfoProvider.Visual; _executingRatio = RefreshInfoProvider.ExecutionRatio; } diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index 0b0c8c99b2..d8e90c01c0 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; using Avalonia.VisualTree; namespace Avalonia.Controls.PullToRefresh @@ -11,9 +12,9 @@ namespace Avalonia.Controls.PullToRefresh private const int InitialOffsetThreshold = 1; private PullDirection _refreshPullDirection; - private ScrollViewer _scrollViewer; - private RefreshInfoProvider _refreshInfoProvider; - private PullGestureRecognizer _pullGestureRecognizer; + private ScrollViewer? _scrollViewer; + private RefreshInfoProvider? _refreshInfoProvider; + private PullGestureRecognizer? _pullGestureRecognizer; private InputElement? _interactionSource; private bool _isVisualizerInteractionSourceAttached; @@ -22,7 +23,7 @@ namespace Avalonia.Controls.PullToRefresh _refreshPullDirection = pullDirection; } - public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) + public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize) { if (root is ScrollViewer scrollViewer) { @@ -44,7 +45,7 @@ namespace Avalonia.Controls.PullToRefresh } } - ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) + ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth) { if (depth == 0) { @@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh return null; } - public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize) { if (adaptee == null) { @@ -121,7 +122,7 @@ namespace Avalonia.Controls.PullToRefresh } } - _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content)); _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); @@ -140,7 +141,7 @@ namespace Avalonia.Controls.PullToRefresh return _refreshInfoProvider; } - private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e) { if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) { @@ -151,9 +152,46 @@ namespace Avalonia.Controls.PullToRefresh } } - private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + public void SetAnimations(RefreshVisualizer refreshVisualizer) { - var content = _scrollViewer.Content as Visual; + var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer); + if (visualizerComposition != null) + { + var compositor = visualizerComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + visualizerComposition.ImplicitAnimations = animation; + } + + if(_scrollViewer != null && _scrollViewer.Content is Visual visual) + { + var scollContentComposition = ElementComposition.GetElementVisual(visual); + + if(scollContentComposition != null) + { + var compositor = scollContentComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + scollContentComposition.ImplicitAnimations = animation; + } + } + } + + private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e) + { + var content = _scrollViewer?.Content as Visual; if (content == null) { throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); @@ -166,23 +204,26 @@ namespace Avalonia.Controls.PullToRefresh MakeInteractionSource(content.Parent as InputElement); - _scrollViewer.Loaded -= ScrollViewer_Loaded; + if (_scrollViewer != null) + { + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } } - private void MakeInteractionSource(InputElement element) + private void MakeInteractionSource(InputElement? element) { _interactionSource = element; - if (_pullGestureRecognizer != null) + if (_pullGestureRecognizer != null && _refreshInfoProvider != null) { - element.GestureRecognizers.Add(_pullGestureRecognizer); - _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); - _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + element?.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); _isVisualizerInteractionSourceAttached = true; } } - private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) + private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e) { if (_refreshInfoProvider != null) { @@ -190,9 +231,12 @@ namespace Avalonia.Controls.PullToRefresh } } - private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) + private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e) { - _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } } private bool IsWithinOffsetThreashold() @@ -219,9 +263,12 @@ namespace Avalonia.Controls.PullToRefresh private void CleanUpScrollViewer() { - _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; - _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; - _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + if (_scrollViewer != null) + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml index 388ca814f8..3fafabb667 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -14,37 +14,7 @@ - - - - - + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml index 0b12fe133d..bd7e43530a 100644 --- a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -18,51 +18,10 @@ MinHeight="80" Background="{TemplateBinding Background}"> - - From 6e1ab5f4578ad7a4edb968f87b203a714c64c0a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 18 Nov 2022 12:37:31 +0000 Subject: [PATCH 05/78] remove debug stuff --- src/Avalonia.Base/Input/PullGestureRecognizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/PullGestureRecognizer.cs index 6939646f13..bbbded44fa 100644 --- a/src/Avalonia.Base/Input/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/PullGestureRecognizer.cs @@ -100,7 +100,7 @@ namespace Avalonia.Input public void PointerPressed(PointerPressedEventArgs e) { - if (_target != null)// && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { var position = e.GetPosition(_target); From 7d6932bd5ef3fca26b44f47911a162212f4bdfeb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Nov 2022 17:04:14 +0000 Subject: [PATCH 06/78] add failing unit tests. --- .../WindowTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 0cdde445d5..f73c3ac215 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -986,7 +986,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } } + + [Fact] + public void IsVisible_Should_Open_Window() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + target.IsVisible = true; + Assert.True(raised); + } + } + + [Fact] + public void IsVisible_Should_Close_DialogWindow() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + parent.Show(); + + var target = new Window(); + + var raised = false; + + var task = target.ShowDialog(parent); + + target.Closed += (sender, args) => raised = true; + + target.IsVisible = false; + + Assert.True(raised); + + Assert.False(task.Result); + } + } + protected virtual void Show(Window window) { window.Show(); From a7a3df912a4984273fd20788de9afe13022d4d39 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Nov 2022 09:46:27 +0000 Subject: [PATCH 07/78] implement isvisible controlling show / hide / close (for dialogs) --- src/Avalonia.Controls/Window.cs | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 559d674c02..672099c3bb 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -72,6 +72,9 @@ namespace Avalonia.Controls private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; + private bool _shown; + private bool _showingAsDialog; + private bool _inShowHideMethods; /// /// Defines the property. @@ -506,8 +509,14 @@ namespace Avalonia.Controls } Owner = null; + _showingAsDialog = false; + _shown = false; + _inShowHideMethods = true; PlatformImpl?.Dispose(); + + _inShowHideMethods = false; + } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -563,7 +572,7 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!IsVisible) + if (!_shown) { return; } @@ -585,7 +594,10 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Hide(); + _shown = false; + _inShowHideMethods = true; IsVisible = false; + _inShowHideMethods = false; } /// @@ -639,7 +651,7 @@ namespace Avalonia.Controls } } - if (IsVisible) + if (_shown) { return; } @@ -648,7 +660,10 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + _inShowHideMethods = true; + _shown = true; IsVisible = true; + _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, @@ -728,7 +743,11 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + _shown = true; + _showingAsDialog = true; + _inShowHideMethods = true; IsVisible = true; + _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? ClientSize.Width : Width, @@ -999,6 +1018,33 @@ namespace Avalonia.Controls PlatformImpl?.SetSystemDecorations(typedNewValue); } + + if (change.Property == IsVisibleProperty) + { + if (!_inShowHideMethods) + { + var isVisible = change.GetNewValue(); + + if (_shown != isVisible) + { + if (!_shown) + { + ShowCore(null); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } } protected override AutomationPeer OnCreateAutomationPeer() From f4804551316b4141367f3ef3fa38c50d9a702fc0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Nov 2022 12:39:24 +0100 Subject: [PATCH 08/78] Added failing test for #9565. And another (passing) test for checking that window order is correct when a child window is shown in fullscreen. --- samples/IntegrationTestApp/MainWindow.axaml | 7 +- .../IntegrationTestApp/MainWindow.axaml.cs | 3 + .../WindowTests_MacOS.cs | 74 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 038ced4e5c..801ff765c8 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -17,11 +17,15 @@ - + + + WindowState: + + @@ -136,6 +140,7 @@ + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index c1acc7ca88..841947673a 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia; @@ -178,6 +179,8 @@ namespace IntegrationTestApp ShowWindow(); if (source?.Name == "SendToBack") SendToBack(); + if (source?.Name == "EnterFullscreen") + WindowState = WindowState.FullScreen; if (source?.Name == "ExitFullscreen") WindowState = WindowState.Normal; if (source?.Name == "RestoreAll") diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 05ed0616a8..8f4417a451 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using Avalonia.Controls; +using Avalonia.Utilities; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; @@ -114,6 +115,79 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(1, secondaryWindowIndex); } } + + [PlatformFact(TestPlatforms.MacOS)] + public void WindowOrder_Owned_Dialog_Stays_InFront_Of_FullScreen_Parent() + { + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + + // Enter fullscreen + mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click(); + + // Wait for fullscreen transition. + Thread.Sleep(1000); + + // Make sure we entered fullscreen. + var windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("FullScreen", windowState.Text); + + // Open child window. + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + mainWindow.SendClick(); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); + } + + // Exit fullscreen by menu shortcut Command+R + mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + + // Wait for restore transition. + Thread.Sleep(1000); + + // Make sure we exited fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("Normal", windowState.Text); + } + + [PlatformFact(TestPlatforms.MacOS)] + public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() + { + // Issue #9565 + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + AppiumWebElement windowState; + + // Open child window. + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + // Enter fullscreen + mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click(); + + // Wait for fullscreen transition. + Thread.Sleep(1000); + + // Make sure we entered fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("FullScreen", windowState.Text); + + // Click on main window + mainWindow.Click(); + + // Failed here due to #9565: main window is no longer visible as the main space is now shown instead + // of the fullscreen space. + mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + + // Wait for restore transition. + Thread.Sleep(1000); + } + + // Make sure we exited fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("Normal", windowState.Text); + } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() From d00f19d55671373ea0329e35e4a52afb3815364b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Nov 2022 12:40:02 +0100 Subject: [PATCH 09/78] Fix #9565 - Only bring window to front if it's on the currently active space - Ensure correct order of child windows after fullscreen transition --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 2443965957..b6dacb6ce4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -137,7 +137,11 @@ void WindowImpl::BringToFront() for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) { - (*iterator)->BringToFront(); + auto window = (*iterator)->Window; + + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) + (*iterator)->BringToFront(); } } } @@ -161,6 +165,9 @@ void WindowImpl::StartStateTransition() { void WindowImpl::EndStateTransition() { _transitioningWindowState = false; + + // Ensure correct order of child windows after fullscreen transition. + BringToFront(); } SystemDecorations WindowImpl::Decorations() { From fdf76c5765ceb8f2c4191aa1257c2eb6cea3bccd Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 30 Nov 2022 14:50:27 +0200 Subject: [PATCH 10/78] Replace IVisual with Visual --- nukebuild/Numerge | 2 +- src/Avalonia.Base/Animation/CrossFade.cs | 2 +- src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs | 2 +- src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs | 2 +- src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs | 2 +- src/Avalonia.Base/Media/MatrixTransform.cs | 2 +- src/Avalonia.Base/Media/RotateTransform.cs | 2 +- src/Avalonia.Base/Media/ScaleTransform.cs | 2 +- src/Avalonia.Base/Media/SkewTransform.cs | 2 +- src/Avalonia.Base/Media/Transform.cs | 2 +- src/Avalonia.Base/Media/TranslateTransform.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nukebuild/Numerge b/nukebuild/Numerge index aef10ae67d..fb92f917cd 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 +Subproject commit fb92f917cd2d3aaec0d2294635d922184ff1e0fc diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 608a0880ec..a229bc7ce6 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Animation { /// - /// Defines a cross-fade animation between two s. + /// Defines a cross-fade animation between two s. /// public class CrossFade : IPageTransition { diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index c4508c3f5c..d56711ad68 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media.Imaging { /// - /// A bitmap that holds the rendering of a . + /// A bitmap that holds the rendering of a . /// public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs index d5ff2b8317..4478504eca 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs @@ -3,7 +3,7 @@ namespace Avalonia.Media.Immutable { /// - /// Represents a transform on an . + /// Represents a transform on an . /// public class ImmutableTransform : ITransform { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 1e0133c9b7..9b443391c5 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media.Immutable { /// - /// Paints an area with an . + /// Paints an area with an . /// internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush { diff --git a/src/Avalonia.Base/Media/MatrixTransform.cs b/src/Avalonia.Base/Media/MatrixTransform.cs index 4e60e1e290..c61acb730c 100644 --- a/src/Avalonia.Base/Media/MatrixTransform.cs +++ b/src/Avalonia.Base/Media/MatrixTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Transforms an according to a . + /// Transforms an according to a . /// public class MatrixTransform : Transform { diff --git a/src/Avalonia.Base/Media/RotateTransform.cs b/src/Avalonia.Base/Media/RotateTransform.cs index 126bb7c274..3bd409149c 100644 --- a/src/Avalonia.Base/Media/RotateTransform.cs +++ b/src/Avalonia.Base/Media/RotateTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Rotates an . + /// Rotates an . /// public class RotateTransform : Transform { diff --git a/src/Avalonia.Base/Media/ScaleTransform.cs b/src/Avalonia.Base/Media/ScaleTransform.cs index 259b23cbd2..d4c1a7f993 100644 --- a/src/Avalonia.Base/Media/ScaleTransform.cs +++ b/src/Avalonia.Base/Media/ScaleTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Scale an . + /// Scale an . /// public class ScaleTransform : Transform { diff --git a/src/Avalonia.Base/Media/SkewTransform.cs b/src/Avalonia.Base/Media/SkewTransform.cs index a96710e331..066f5371c3 100644 --- a/src/Avalonia.Base/Media/SkewTransform.cs +++ b/src/Avalonia.Base/Media/SkewTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Skews an . + /// Skews an . /// public class SkewTransform : Transform { diff --git a/src/Avalonia.Base/Media/Transform.cs b/src/Avalonia.Base/Media/Transform.cs index 023a8b9cdd..85393ab189 100644 --- a/src/Avalonia.Base/Media/Transform.cs +++ b/src/Avalonia.Base/Media/Transform.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Represents a transform on an . + /// Represents a transform on an . /// public abstract class Transform : Animatable, IMutableTransform { diff --git a/src/Avalonia.Base/Media/TranslateTransform.cs b/src/Avalonia.Base/Media/TranslateTransform.cs index d6d6809f3d..0f910f3600 100644 --- a/src/Avalonia.Base/Media/TranslateTransform.cs +++ b/src/Avalonia.Base/Media/TranslateTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Translates (moves) an . + /// Translates (moves) an . /// public class TranslateTransform : Transform { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs index 4eec214f4f..a991f2f657 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { /// - /// A node in the low-level scene graph representing an . + /// A node in the low-level scene graph representing an . /// internal class VisualNode : IVisualNode { From 8c019e669194784a29243a4922b6beb98dbf99e2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 14:15:25 +0000 Subject: [PATCH 11/78] simpler fix. --- src/Avalonia.Controls/Window.cs | 47 ++------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 672099c3bb..280853967a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -72,9 +72,6 @@ namespace Avalonia.Controls private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; - private bool _shown; - private bool _showingAsDialog; - private bool _inShowHideMethods; /// /// Defines the property. @@ -179,6 +176,7 @@ namespace Avalonia.Controls private object? _dialogResult; private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; + private bool _shown; /// /// Initializes static members of the class. @@ -509,14 +507,8 @@ namespace Avalonia.Controls } Owner = null; - _showingAsDialog = false; - _shown = false; - _inShowHideMethods = true; PlatformImpl?.Dispose(); - - _inShowHideMethods = false; - } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -594,10 +586,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Hide(); - _shown = false; - _inShowHideMethods = true; IsVisible = false; - _inShowHideMethods = false; + _shown = false; } /// @@ -660,10 +650,8 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); - _inShowHideMethods = true; _shown = true; IsVisible = true; - _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, @@ -743,11 +731,7 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); - _shown = true; - _showingAsDialog = true; - _inShowHideMethods = true; IsVisible = true; - _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? ClientSize.Width : Width, @@ -1018,33 +1002,6 @@ namespace Avalonia.Controls PlatformImpl?.SetSystemDecorations(typedNewValue); } - - if (change.Property == IsVisibleProperty) - { - if (!_inShowHideMethods) - { - var isVisible = change.GetNewValue(); - - if (_shown != isVisible) - { - if (!_shown) - { - ShowCore(null); - } - else - { - if (_showingAsDialog) - { - Close(false); - } - else - { - Hide(); - } - } - } - } - } } protected override AutomationPeer OnCreateAutomationPeer() From fb821c7899ca6ca1e900526df25d74326dddbb23 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 14:26:34 +0000 Subject: [PATCH 12/78] ensure that isvisible = false can close dialogs. --- src/Avalonia.Controls/Window.cs | 230 +++++++++++++++++----------- src/Avalonia.Controls/WindowBase.cs | 18 +-- 2 files changed, 149 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 280853967a..aa0ce50bad 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -177,6 +177,7 @@ namespace Avalonia.Controls private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; private bool _shown; + private bool _showingAsDialog; /// /// Initializes static members of the class. @@ -509,6 +510,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Dispose(); + + _showingAsDialog = false; } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -601,6 +604,33 @@ namespace Avalonia.Controls ShowCore(null); } + protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!IgnoreVisibilityChange) + { + var isVisible = e.GetNewValue(); + + if (_shown != isVisible) + { + if(!_shown) + { + Show(); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } + /// /// Shows the window as a child of . /// @@ -620,63 +650,72 @@ namespace Avalonia.Controls private void ShowCore(Window? parent) { - if (PlatformImpl == null) - { - throw new InvalidOperationException("Cannot re-show a closed window."); - } - - if (parent != null) + try { - if (parent.PlatformImpl == null) + IgnoreVisibilityChange = true; + + if (PlatformImpl == null) { - throw new InvalidOperationException("Cannot show a window with a closed parent."); + throw new InvalidOperationException("Cannot re-show a closed window."); } - else if (parent == this) + + if (parent != null) { - throw new InvalidOperationException("A Window cannot be its own parent."); + if (parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed parent."); + } + else if (parent == this) + { + throw new InvalidOperationException("A Window cannot be its own parent."); + } + else if (!parent.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } } - else if (!parent.IsVisible) + + if (_shown) { - throw new InvalidOperationException("Cannot show window with non-visible parent."); + return; } - } - if (_shown) - { - return; - } - - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - EnsureInitialized(); - ApplyStyling(); - _shown = true; - IsVisible = true; + EnsureInitialized(); + ApplyStyling(); + _shown = true; + IsVisible = true; - var initialSize = new Size( - double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, - double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); + var initialSize = new Size( + double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, + double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - LayoutManager.ExecuteInitialLayoutPass(); + LayoutManager.ExecuteInitialLayoutPass(); - if (PlatformImpl != null && parent?.PlatformImpl is not null) - { - PlatformImpl.SetParent(parent.PlatformImpl); - } + if (PlatformImpl != null && parent?.PlatformImpl is not null) + { + PlatformImpl.SetParent(parent.PlatformImpl); + } - Owner = parent; - parent?.AddChild(this, false); + Owner = parent; + parent?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(parent?.PlatformImpl); - PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); - OnOpened(EventArgs.Empty); + PlatformImpl?.Show(ShowActivated, false); + Renderer?.Start(); + OnOpened(EventArgs.Empty); + } + finally + { + IgnoreVisibilityChange = false; + } } /// @@ -706,68 +745,79 @@ namespace Avalonia.Controls /// public Task ShowDialog(Window owner) { - if (owner == null) - { - throw new ArgumentNullException(nameof(owner)); - } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) + try { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + IgnoreVisibilityChange = true; + + if (owner == null) + { + throw new ArgumentNullException(nameof(owner)); + } + else if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + else if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + else if (IsVisible) + { + throw new InvalidOperationException("The window is already being shown."); + } + else if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - EnsureInitialized(); - ApplyStyling(); - IsVisible = true; + EnsureInitialized(); + ApplyStyling(); + _shown = true; + _showingAsDialog = true; + IsVisible = true; - var initialSize = new Size( - double.IsNaN(Width) ? ClientSize.Width : Width, - double.IsNaN(Height) ? ClientSize.Height : Height); + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - LayoutManager.ExecuteInitialLayoutPass(); + LayoutManager.ExecuteInitialLayoutPass(); - var result = new TaskCompletionSource(); + var result = new TaskCompletionSource(); - PlatformImpl?.SetParent(owner.PlatformImpl); - Owner = owner; - owner.AddChild(this, true); + PlatformImpl?.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this, true); - SetWindowStartupLocation(owner.PlatformImpl); + SetWindowStartupLocation(owner.PlatformImpl); - PlatformImpl?.Show(ShowActivated, true); + PlatformImpl?.Show(ShowActivated, true); - Renderer?.Start(); + Renderer?.Start(); - Observable.FromEventPattern( - x => Closed += x, - x => Closed -= x) - .Take(1) - .Subscribe(_ => - { - owner.Activate(); - result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); - }); + Observable.FromEventPattern( + x => Closed += x, + x => Closed -= x) + .Take(1) + .Subscribe(_ => + { + owner.Activate(); + result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); + }); - OnOpened(EventArgs.Empty); - return result.Task; + OnOpened(EventArgs.Empty); + return result.Task; + } + finally + { + IgnoreVisibilityChange = false; + } } private void UpdateEnabled() diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 8f1b2198ad..46653c8203 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls private bool _hasExecutedInitialLayoutPass; private bool _isActive; - private bool _ignoreVisibilityChange; + protected bool IgnoreVisibilityChange { get; set; } private WindowBase? _owner; static WindowBase() @@ -125,7 +125,7 @@ namespace Avalonia.Controls /// public virtual void Hide() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -135,7 +135,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -144,7 +144,7 @@ namespace Avalonia.Controls /// public virtual void Show() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -163,7 +163,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -202,7 +202,7 @@ namespace Avalonia.Controls protected override void HandleClosed() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -217,7 +217,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -318,9 +318,9 @@ namespace Avalonia.Controls Deactivated?.Invoke(this, EventArgs.Empty); } - private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!_ignoreVisibilityChange) + if (!IgnoreVisibilityChange) { if ((bool)e.NewValue!) { From 4a490694a24f7e0cd2eff83cd7832fb2f18655f9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 20:08:42 +0000 Subject: [PATCH 13/78] fix unit test. --- src/Avalonia.Controls/Window.cs | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index aa0ce50bad..1ff08f0169 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -567,30 +567,39 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!_shown) + try { - return; - } + IgnoreVisibilityChange = true; + + if (!_shown) + { + return; + } - Renderer?.Stop(); + Renderer?.Stop(); - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); + } - if (_children.Count > 0) - { - foreach (var child in _children.ToArray()) + if (_children.Count > 0) { - child.child.Hide(); + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } } - } - Owner = null; - PlatformImpl?.Hide(); - IsVisible = false; - _shown = false; + Owner = null; + PlatformImpl?.Hide(); + IsVisible = false; + _shown = false; + } + finally + { + IgnoreVisibilityChange = false; + } } /// From 390221b3cee6173161e620da648081b477de9206 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Nov 2022 22:32:27 -0500 Subject: [PATCH 14/78] Implement compile time MergedDictionaties --- .../Themes/Fluent/ColorPicker.xaml | 4 - .../Themes/Fluent/Fluent.xaml | 10 +- .../Themes/Simple/ColorPicker.xaml | 4 - .../Themes/Simple/Simple.xaml | 10 +- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 1 + .../Controls/Button.xaml | 2 - .../Controls/FluentControls.xaml | 126 +++++++++--------- .../Controls/RepeatButton.xaml | 2 - .../Controls/ToggleButton.xaml | 2 - src/Avalonia.Themes.Fluent/FluentTheme.xaml | 4 +- .../FluentTheme.xaml.cs | 8 +- .../Controls/SimpleControls.xaml | 124 ++++++++--------- src/Avalonia.Themes.Simple/SimpleTheme.xaml | 2 +- .../SimpleTheme.xaml.cs | 4 +- .../AvaloniaRuntimeXamlLoader.cs | 4 +- .../AvaloniaXamlIlCompiler.cs | 1 + .../IXamlAstGroupTransformer.cs | 2 +- .../XamlIncludeGroupTransformer.cs | 75 +++++++---- .../XamlMergeResourceGroupTransformer.cs | 113 ++++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../XamlDocumentParseException.cs | 5 + .../Avalonia.Markup.Xaml.csproj | 1 + .../Styling/MergeResourceInclude.cs | 69 ++++++++++ .../Xaml/MergeResourceIncludeTests.cs | 124 +++++++++++++++++ 24 files changed, 515 insertions(+), 184 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 9d2a994e5b..2ccf20d460 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index 85f4e417e6..2cc8a1d38a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 626ddd4b43..b82d36a288 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml index 7aefa23706..8c4dfa9c87 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 259d107b5c..479bcd8531 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -22,6 +22,7 @@ 10,6,6,5 20 20 + 8,5,8,6 3 diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7828fd52ed..126f2c22e0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -8,8 +8,6 @@ - - 8,5,8,6 diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index a029be6b8d..2733365479 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -4,70 +4,70 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index a54187104b..fd04c85fed 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index 7a46f21534..da2021790a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index d8f8267fe5..44ca60e2fa 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -4,8 +4,8 @@ - - + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 728e81b198..80460e1bde 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -97,15 +97,15 @@ namespace Avalonia.Themes.Fluent var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 2) + if (dict.Count == 0) { - dict.Insert(1, themeVariantResource1); + dict.Add(themeVariantResource1); dict.Add(themeVariantResource2); } else { - dict[1] = themeVariantResource1; - dict[3] = themeVariantResource2; + dict[0] = themeVariantResource1; + dict[1] = themeVariantResource2; } } diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4aefa0136c..2d7fdcdd50 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -3,68 +3,68 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml index fe296bd288..5b0cae7fd2 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -4,7 +4,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index af9d305043..e452646eab 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -56,13 +56,13 @@ namespace Avalonia.Themes.Simple { var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 1) + if (dict.Count == 0) { dict.Add(themeVariantResource); } else { - dict[1] = themeVariantResource; + dict[0] = themeVariantResource; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs index b4c951fc5e..6f6420f66d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -56,8 +56,8 @@ namespace Avalonia.Markup.Xaml /// /// Collection of documents. /// Xaml loader configuration. - /// The loaded objects per each input document. - public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) + /// The loaded objects per each input document. If document was removed, the element by index is null. + public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) => AvaloniaXamlIlRuntimeCompiler.LoadGroup(documents, configuration ?? new RuntimeXamlLoaderConfiguration()); /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b8350c3f11..aaaee39b0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -85,6 +85,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions GroupTransformers = new() { + new XamlMergeResourceGroupTransformer(), new AvaloniaXamlIncludeTransformer() }; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs index 32bf37431f..eeb5293325 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs @@ -18,7 +18,7 @@ internal class AstGroupTransformationContext : AstTransformationContext public IXamlDocumentResource CurrentDocument { get; set; } public IReadOnlyCollection Documents { get; } - + public new IXamlAstNode ParseError(string message, IXamlAstNode node) => Error(node, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, node)); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index cc29d5ccb5..813c135dc6 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -15,6 +15,7 @@ using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) @@ -34,40 +35,26 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); } - + if (valueNode.Manipulation is not XamlObjectInitializationNode { Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty }) { - return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); + throw new XamlDocumentParseException(context.CurrentDocument, + $"Source property must be set on the \"{nodeTypeName}\" node.", valueNode); } - // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. - if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode - || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri - || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } - || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false); + if (assetPathUri is null) { - // TODO: make it a compiler warning - // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. return node; } - - var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); - if (!uriPath.IsAbsoluteUri) + else { - var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); - uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); - } - else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) - { - return context.ParseError( - $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", - sourceUriNode, node); + sourceUriNode ??= valueNode; } - var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri); var assetPath = assetPathUri.Replace("avares://", ""); var assemblyNameSeparator = assetPath.IndexOf('/'); var assembly = assetPath.Substring(0, assemblyNameSeparator); @@ -119,7 +106,7 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer $"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.", sourceUriNode, node); } - + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) { @@ -151,7 +138,49 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer new[] { new NewServiceProviderNode(sp, li) }); } - internal class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude( + AstGroupTransformationContext context, string nodeTypeName, XamlPropertyAssignmentNode sourceProperty, + bool strictSourceValueType) + { + // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode + || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } + || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + { + // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. + var anyPropValue = sourceProperty.Values.FirstOrDefault(); + if (strictSourceValueType) + { + context.Error(anyPropValue, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", anyPropValue)); + } + else + { + // TODO: make it a compiler warning + } + return (null, anyPropValue); + } + + var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); + if (!uriPath.IsAbsoluteUri) + { + var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); + uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); + } + else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) + { + context.Error(sourceUriNode, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", sourceUriNode)); + return (null, sourceUriNode); + } + + return (Uri.UnescapeDataString(uriPath.AbsoluteUri), sourceUriNode); + } + + private class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, IXamlAstEmitableNode { public NewServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs new file mode 100644 index 0000000000..8c83c74248 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.IL.Emitters; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable + +internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer +{ + public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) + { + var resourceDictionaryType = context.GetAvaloniaTypes().ResourceDictionary; + if (node is not XamlObjectInitializationNode resourceDictionaryNode + || resourceDictionaryNode.Type != resourceDictionaryType + || resourceDictionaryNode.Manipulation is not XamlManipulationGroupNode resourceDictionaryManipulation) + { + return node; + } + + var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; + var mergeSourceNodes = new List(); + var hasAnyNonMergedResource = false; + foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) + { + void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) + { + if (assignmentNode.Property.Name == "MergedDictionaries" + && assignmentNode.Values.FirstOrDefault() is XamlValueWithManipulationNode valueNode) + { + if (valueNode.Type.GetClrType() == mergeResourceIncludeType) + { + if (valueNode.Manipulation is XamlObjectInitializationNode objectInitialization + && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) + { + parent.Children.Remove(assignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + } + else + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Invalid MergeResourceInclude node found. Make sure that Source property is set.", + valueNode); + } + } + else + { + hasAnyNonMergedResource = true; + } + + if (hasAnyNonMergedResource && mergeSourceNodes.Any()) + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", + valueNode); + } + } + } + + if (manipulationNode is XamlPropertyAssignmentNode singleValueAssignment) + { + ProcessXamlPropertyAssignmentNode(resourceDictionaryManipulation, singleValueAssignment); + } + else if (manipulationNode is XamlManipulationGroupNode groupNodeValues) + { + foreach (var groupNodeValue in groupNodeValues.Children.OfType().ToArray()) + { + ProcessXamlPropertyAssignmentNode(groupNodeValues, groupNodeValue); + } + } + } + + var manipulationGroup = new XamlManipulationGroupNode(node, new List()); + foreach (var sourceNode in mergeSourceNodes) + { + var (originalAssetPath, propertyNode) = + AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); + if (originalAssetPath is null) + { + return node; + } + + var targetDocument = context.Documents.FirstOrDefault(d => + string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) + ?.XamlDocument.Root as XamlValueWithManipulationNode; + if (targetDocument is null) + { + return context.ParseError( + $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); + } + + var singleRootObject = ((XamlManipulationGroupNode)targetDocument.Manipulation) + .Children.OfType().Single(); + if (singleRootObject.Type != resourceDictionaryType) + { + return context.ParseError( + $"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); + } + + manipulationGroup.Children.Add(singleRootObject.Manipulation); + } + + if (manipulationGroup.Children.Any()) + { + // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. + resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); + } + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 87a037c16a..5753a1008c 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -104,6 +104,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IStyle { get; } public IXamlType StyleInclude { get; } public IXamlType ResourceInclude { get; } + public IXamlType MergeResourceInclude { get; } public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } @@ -236,6 +237,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude"); ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude"); + MergeResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.MergeResourceInclude"); IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs index d031a6086b..0532287a67 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs @@ -18,4 +18,9 @@ internal class XamlDocumentParseException : XamlParseException { FilePath = path; } + + public XamlDocumentParseException(IXamlDocumentResource document, string message, IXamlLineInfo lineInfo) + : this(document.FileSource?.FilePath, message, lineInfo) + { + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 0ab00007e7..4c3aaa4ec0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs new file mode 100644 index 0000000000..c81a3c1416 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -0,0 +1,69 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Markup.Xaml.Styling; + +public class MergeResourceInclude : IResourceProvider +{ + private readonly IServiceProvider _serviceProvider; + private readonly Uri? _baseUri; + private IResourceDictionary? _loaded; + private bool _isLoading; + + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. + public MergeResourceInclude(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _baseUri = serviceProvider.GetContextBaseUri(); + } + + /// + /// Gets the loaded resource dictionary. + /// + public IResourceDictionary Loaded + { + get + { + if (_loaded == null) + { + _isLoading = true; + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, Source, _baseUri); + _isLoading = false; + } + + return _loaded; + } + } + + public IResourceHost? Owner => Loaded.Owner; + + /// + /// Gets or sets the source URL. + /// + public Uri? Source { get; set; } + + bool IResourceNode.HasResources => Loaded.HasResources; + + public event EventHandler? OwnerChanged + { + add => Loaded.OwnerChanged += value; + remove => Loaded.OwnerChanged -= value; + } + + bool IResourceNode.TryGetResource(object key, out object? value) + { + if (!_isLoading) + { + return Loaded.TryGetResource(key, out value); + } + + value = null; + return false; + } + + void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); + void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner); +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs new file mode 100644 index 0000000000..520abee59a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Xml; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml; + +public class MergeResourceIncludeTests +{ + [Fact] + public void MergeResourceInclude_Works_With_Single_Resource() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(@" + + + + Blue + + + + + +") + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var contentControl = Assert.IsType(objects[1]); + + var resources = Assert.IsType(contentControl.Resources); + Assert.Empty(resources.MergedDictionaries); + + var initialResource = (ISolidColorBrush)resources["brush1"]!; + Assert.Equal(Colors.Blue, initialResource.Color); + + var mergedResource = (ISolidColorBrush)resources["brush2"]!; + Assert.Equal(Colors.Red, mergedResource.Color); + } + + [Fact] + public void Mixing_MergeResourceInclude_And_ResourceInclude_Is_Not_Allowed() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); + } + + [Fact] + public void MergeResourceInclude_Works_With_Multiple_Resources() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red + Blue +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Yellow + + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + + Black + White +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1_2.xaml"), @" + + Green +"), + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var resources = Assert.IsType(objects[2]); + Assert.Empty(resources.MergedDictionaries); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)resources["brush1"]!).Color); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)resources["brush2"]!).Color); + Assert.Equal(Colors.Green, ((ISolidColorBrush)resources["brush3"]!).Color); + Assert.Equal(Colors.Yellow, ((ISolidColorBrush)resources["brush4"]!).Color); + Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); + Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); + } +} From c03187ff7ce32d8b64b5b823bf16382d6ed6f013 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 1 Dec 2022 01:14:13 -0500 Subject: [PATCH 15/78] Add a new benchmark --- .../Themes/ThemeBenchmark.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index e82576c7d9..70636d1fe6 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -16,6 +16,8 @@ namespace Avalonia.Benchmarks.Themes public class ThemeBenchmark : IDisposable { private IDisposable _app; + private readonly FluentTheme _reusableFluentTheme = new FluentTheme(); + private readonly SimpleTheme _reusableSimpleTheme = new SimpleTheme(); public ThemeBenchmark() { @@ -49,6 +51,26 @@ namespace Avalonia.Benchmarks.Themes }; return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindFluentControlTheme(Type type) + { + _reusableFluentTheme.TryGetResource(type, out var theme); + return theme; + } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindSimpleControlTheme(Type type) + { + _reusableSimpleTheme.TryGetResource(type, out var theme); + return theme; + } public void Dispose() { From de9e9580c00a0310e95d459ec4ad4b40e30fe9ad Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 25 Nov 2022 21:15:20 -0500 Subject: [PATCH 16/78] Pass parent's service provider to the StyleInclude, so parents stack is complete --- .../BuildEngineErrorCode.cs | 1 + .../XamlCompilerTaskExecutor.cs | 119 ++++++++++++++---- .../FluentTheme.xaml.cs | 8 +- .../SimpleTheme.xaml.cs | 8 +- .../XamlIncludeGroupTransformer.cs | 3 +- ...IlConstructorServiceProviderTransformer.cs | 12 ++ .../AvaloniaXamlLoader.cs | 41 +++++- .../StaticResourceExtension.cs | 21 +--- .../Styling/ResourceInclude.cs | 4 +- .../Styling/StyleInclude.cs | 4 +- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 28 +++-- .../Avalonia.Markup.Xaml.UnitTests.csproj | 1 + .../Xaml/StyleIncludeTests.cs | 43 +++++++ .../Xaml/StyleWithServiceLocator.xaml | 5 + .../Xaml/StyleWithServiceLocator.xaml.cs | 16 +++ 15 files changed, 251 insertions(+), 63 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs diff --git a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs index a149a758f4..a31c9a7516 100644 --- a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs +++ b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs @@ -7,6 +7,7 @@ namespace Avalonia.Build.Tasks LegacyResmScheme = 3, TransformError = 4, EmitError = 4, + Loader = 5, Unknown = 9999 } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index ea2cf2cf99..a394d47904 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -196,7 +196,8 @@ namespace Avalonia.Build.Tasks var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); var createRootServiceProviderMethod = asm.MainModule.ImportReference( typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods - .First(x => x.Name == "CreateRootServiceProviderV2")); + .First(x => x.Name == "CreateRootServiceProviderV3")); + var serviceProviderType = createRootServiceProviderMethod.ReturnType; var loaderDispatcherDef = new TypeDefinition(CompiledAvaloniaXamlNamespace, "!XamlLoader", TypeAttributes.Class | TypeAttributes.Public, asm.MainModule.TypeSystem.Object); @@ -212,11 +213,36 @@ namespace Avalonia.Build.Tasks MethodAttributes.Static | MethodAttributes.Public, asm.MainModule.TypeSystem.Object) { - Parameters = {new ParameterDefinition(asm.MainModule.TypeSystem.String)} + Parameters = + { + new ParameterDefinition(serviceProviderType), + new ParameterDefinition(asm.MainModule.TypeSystem.String) + }, + }; + var loaderDispatcherMethodOld = new MethodDefinition("TryLoad", + MethodAttributes.Static | MethodAttributes.Public, + asm.MainModule.TypeSystem.Object) + { + Parameters = + { + new ParameterDefinition(asm.MainModule.TypeSystem.String) + }, + Body = + { + Instructions = + { + Instruction.Create(OpCodes.Ldnull), + Instruction.Create(OpCodes.Ldarg_0), + Instruction.Create(OpCodes.Call, loaderDispatcherMethod), + Instruction.Create(OpCodes.Ret) + } + } }; loaderDispatcherDef.Methods.Add(loaderDispatcherMethod); + loaderDispatcherDef.Methods.Add(loaderDispatcherMethodOld); asm.MainModule.Types.Add(loaderDispatcherDef); + var stringEquals = asm.MainModule.ImportReference(asm.MainModule.TypeSystem.String.Resolve().Methods.First( m => m.IsStatic && m.Name == "Equals" && m.Parameters.Count == 3 && @@ -377,30 +403,42 @@ namespace Avalonia.Build.Tasks classTypeDefinition.Fields.Add(designLoaderField); const string TrampolineName = "!XamlIlPopulateTrampoline"; - var trampoline = new MethodDefinition(TrampolineName, - MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void); - trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition)); - classTypeDefinition.Methods.Add(trampoline); + var trampolineMethodWithoutSP = new Lazy(() => CreateTrampolineMethod(false)); + var trampolineMethodWithSP = new Lazy(() => CreateTrampolineMethod(true)); + MethodDefinition CreateTrampolineMethod(bool hasSystemProviderArg) + { + var trampoline = new MethodDefinition(TrampolineName, + MethodAttributes.Static | MethodAttributes.Private, asm.MainModule.TypeSystem.Void); + if (hasSystemProviderArg) + { + trampoline.Parameters.Add(new ParameterDefinition(serviceProviderType)); + } + trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition)); + + classTypeDefinition.Methods.Add(trampoline); - var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); + var regularStart = Instruction.Create(OpCodes.Nop); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, designLoaderLoad)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); + trampoline.Body.Instructions.Add(Instruction.Create(hasSystemProviderArg ? OpCodes.Ldarg_1 : OpCodes.Ldarg_0)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, designLoaderLoad)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); - trampoline.Body.Instructions.Add(regularStart); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod)); - trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); - CopyDebugDocument(trampoline, compiledPopulateMethod); + trampoline.Body.Instructions.Add(regularStart); + trampoline.Body.Instructions.Add(Instruction.Create(hasSystemProviderArg ? OpCodes.Ldarg_0 : OpCodes.Ldnull)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod)); + trampoline.Body.Instructions.Add(Instruction.Create(hasSystemProviderArg ? OpCodes.Ldarg_1 : OpCodes.Ldarg_0)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Call, compiledPopulateMethod)); + trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + CopyDebugDocument(trampoline, compiledPopulateMethod); + return trampoline; + } var foundXamlLoader = false; - // Find AvaloniaXamlLoader.Load(this) and replace it with !XamlIlPopulateTrampoline(this) - foreach (var method in classTypeDefinition.Methods - .Where(m => !m.Attributes.HasFlag(MethodAttributes.Static))) + // Find AvaloniaXamlLoader.Load(this) or AvaloniaXamlLoader.Load(sp, this) and replace it with !XamlIlPopulateTrampoline(this) + foreach (var method in classTypeDefinition.Methods.ToArray()) { var i = method.Body.Instructions; for (var c = 1; c < i.Count; c++) @@ -422,7 +460,20 @@ namespace Avalonia.Build.Tasks { if (MatchThisCall(i, c - 1)) { - i[c].Operand = trampoline; + i[c].Operand = trampolineMethodWithoutSP.Value; + foundXamlLoader = true; + } + } + if (op != null + && op.Name == "Load" + && op.Parameters.Count == 2 + && op.Parameters[0].ParameterType.FullName == "System.IServiceProvider" + && op.Parameters[1].ParameterType.FullName == "System.Object" + && op.DeclaringType.FullName == "Avalonia.Markup.Xaml.AvaloniaXamlLoader") + { + if (MatchThisCall(i, c - 1)) + { + i[c].Operand = trampolineMethodWithSP.Value; foundXamlLoader = true; } } @@ -439,7 +490,7 @@ namespace Avalonia.Build.Tasks { var i = ctors[0].Body.Instructions; var retIdx = i.IndexOf(i.Last(x => x.OpCode == OpCodes.Ret)); - i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampoline)); + i.Insert(retIdx, Instruction.Create(OpCodes.Call, trampolineMethodWithoutSP.Value)); i.Insert(retIdx, Instruction.Create(OpCodes.Ldarg_0)); } else @@ -461,20 +512,33 @@ namespace Avalonia.Build.Tasks null : classTypeDefinition.GetConstructors().FirstOrDefault(c => c.IsPublic && !c.IsStatic && !c.HasParameters); + var constructorWithSp = compiledBuildMethod != null ? + null : + classTypeDefinition.GetConstructors().FirstOrDefault(c => + c.IsPublic && !c.IsStatic && c.Parameters.Count == 1 && c.Parameters[0].ParameterType.FullName == serviceProviderType.FullName); - if (compiledBuildMethod != null || parameterlessConstructor != null) + if (compiledBuildMethod != null || parameterlessConstructor != null || constructorWithSp != null) { var i = loaderDispatcherMethod.Body.Instructions; var nop = Instruction.Create(OpCodes.Nop); - i.Add(Instruction.Create(OpCodes.Ldarg_0)); + i.Add(Instruction.Create(OpCodes.Ldarg_1)); i.Add(Instruction.Create(OpCodes.Ldstr, res.Uri)); i.Add(Instruction.Create(OpCodes.Ldc_I4, (int)StringComparison.OrdinalIgnoreCase)); i.Add(Instruction.Create(OpCodes.Call, stringEquals)); i.Add(Instruction.Create(OpCodes.Brfalse, nop)); if (parameterlessConstructor != null) + { i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor)); + } + else if (constructorWithSp != null) + { + i.Add(Instruction.Create(OpCodes.Ldarg_0)); + i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod)); + i.Add(Instruction.Create(OpCodes.Newobj, constructorWithSp)); + } else { + i.Add(Instruction.Create(OpCodes.Ldarg_0)); i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod)); i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod)); } @@ -482,6 +546,11 @@ namespace Avalonia.Build.Tasks i.Add(Instruction.Create(OpCodes.Ret)); i.Add(nop); } + else + { + engine.LogWarning(BuildEngineErrorCode.Loader, "", + $"XAML resource \"{res.Uri}\" won't be reachable via runtime loader, as no public constructor was found"); + } } } diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 728e81b198..eea3d3ad08 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -31,9 +32,10 @@ namespace Avalonia.Themes.Fluent /// /// Initializes a new instance of the class. /// - public FluentTheme() + /// The parent's service provider. + public FluentTheme(IServiceProvider? sp = null) { - AvaloniaXamlLoader.Load(this); + AvaloniaXamlLoader.Load(sp, this); _baseDark = (IResourceDictionary)GetAndRemove("BaseDark"); _fluentDark = (IResourceDictionary)GetAndRemove("FluentDark"); diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index af9d305043..56c4cbeac6 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -16,9 +17,10 @@ namespace Avalonia.Themes.Simple /// /// Initializes a new instance of the class. /// - public SimpleTheme() + /// The parent's service provider. + public SimpleTheme(IServiceProvider? sp = null) { - AvaloniaXamlLoader.Load(this); + AvaloniaXamlLoader.Load(sp, this); _simpleDark = (IResourceDictionary)GetAndRemove("BaseDark"); _simpleLight = (IResourceDictionary)GetAndRemove("BaseLight"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index cc29d5ccb5..1bb05e238c 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -163,8 +163,9 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer public bool NeedsParentStack => true; public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { + codeGen.Ldloc(context.ContextLocal); var method = context.GetAvaloniaTypes().RuntimeHelpers - .FindMethod(m => m.Name == "CreateRootServiceProviderV2"); + .FindMethod(m => m.Name == "CreateRootServiceProviderV3"); codeGen.EmitCall(method); return XamlILNodeEmitResult.Type(0, Type.GetClrType()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 35e2624ff9..0304165995 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs @@ -41,6 +41,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public bool NeedsParentStack => true; public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { + if (_inheritContext) + { + codeGen.Ldloc(context.ContextLocal); + } + else + { + codeGen.Ldloc(context.ContextLocal); + var method = context.GetAvaloniaTypes().RuntimeHelpers + .FindMethod(m => m.Name == "CreateRootServiceProviderV3"); + codeGen.EmitCall(method); + } + codeGen.Ldloc(context.ContextLocal); return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 388ff61400..b5d222d979 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -23,6 +23,17 @@ namespace Avalonia.Markup.Xaml throw new XamlLoadException( $"No precompiled XAML found for {obj.GetType()}, make sure to specify x:Class and include your XAML file as AvaloniaResource"); } + + /// + /// Loads the XAML into a Avalonia component. + /// + /// The parent's service provider. + /// The object to load the XAML into. + public static void Load(IServiceProvider? sp, object obj) + { + throw new XamlLoadException( + $"No precompiled XAML found for {obj.GetType()}, make sure to specify x:Class and include your XAML file as AvaloniaResource"); + } /// /// Loads XAML from a URI. @@ -33,6 +44,20 @@ namespace Avalonia.Markup.Xaml /// /// The loaded object. public static object Load(Uri uri, Uri? baseUri = null) + { + return Load(null, uri, baseUri); + } + + /// + /// Loads XAML from a URI. + /// + /// The parent's service provider. + /// The URI of the XAML file. + /// + /// A base URI to use if is relative. + /// + /// The loaded object. + public static object Load(IServiceProvider? sp, Uri uri, Uri? baseUri = null) { if (uri is null) throw new ArgumentNullException(nameof(uri)); @@ -51,13 +76,25 @@ namespace Avalonia.Markup.Xaml var compiledLoader = assetLocator.GetAssembly(uri, baseUri) ?.GetType("CompiledAvaloniaXaml.!XamlLoader") - ?.GetMethod("TryLoad", new[] {typeof(string)}); + ?.GetMethod("TryLoad", new[] { typeof(System.IServiceProvider), typeof(string) }); if (compiledLoader != null) { - var compiledResult = compiledLoader.Invoke(null, new object[] {absoluteUri.ToString()}); + var compiledResult = compiledLoader.Invoke(null, new object?[] { sp, absoluteUri.ToString()}); if (compiledResult != null) return compiledResult; } + else + { + compiledLoader = assetLocator.GetAssembly(uri, baseUri) + ?.GetType("CompiledAvaloniaXaml.!XamlLoader") + ?.GetMethod("TryLoad", new[] {typeof(string)}); + if (compiledLoader != null) + { + var compiledResult = compiledLoader.Invoke(null, new object?[] {absoluteUri.ToString()}); + if (compiledResult != null) + return compiledResult; + } + } // This is intended for unit-tests only var runtimeLoader = AvaloniaLocator.Current.GetService(); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 5bff9d9ca6..84b4f3bdba 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -34,13 +34,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions _ => null, }; - if (provideTarget.TargetObject is Setter setter) + if (provideTarget.TargetObject is Setter { Property: not null } setter) { targetType = setter.Property.PropertyType; } - - var previousWasControlTheme = false; - + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. foreach (var parent in stack.Parents) @@ -49,21 +47,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { return ColorToBrushConverter.Convert(value, targetType); } - - // HACK: Temporary fix for #8678. Hard-coded to only work for the DevTools main - // window as we don't want 3rd parties to start relying on this hack. - // - // We need to implement compile-time merging of resource dictionaries and this - // hack can be removed. - if (previousWasControlTheme && - parent is IResourceProvider hack && - hack.Owner?.GetType().FullName == "Avalonia.Diagnostics.Views.MainWindow" && - hack.Owner.TryGetResource(ResourceKey, out value)) - { - return ColorToBrushConverter.Convert(value, targetType); - } - - previousWasControlTheme = parent is ControlTheme; } if (provideTarget.TargetObject is Control target && diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index d7eb328715..8bbf233ed9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -10,6 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling /// public class ResourceInclude : IResourceProvider { + private readonly IServiceProvider _serviceProvider; private readonly Uri? _baseUri; private IResourceDictionary? _loaded; private bool _isLoading; @@ -29,6 +30,7 @@ namespace Avalonia.Markup.Xaml.Styling /// The XAML service provider. public ResourceInclude(IServiceProvider serviceProvider) { + _serviceProvider = serviceProvider; _baseUri = serviceProvider.GetContextBaseUri(); } @@ -43,7 +45,7 @@ namespace Avalonia.Markup.Xaml.Styling { _isLoading = true; var source = Source ?? throw new InvalidOperationException("ResourceInclude.Source must be set."); - _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, source, _baseUri); _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 2b75e0dd54..30e8ef4d02 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -12,6 +12,7 @@ namespace Avalonia.Markup.Xaml.Styling /// public class StyleInclude : IStyle, IResourceProvider { + private readonly IServiceProvider _serviceProvider; private readonly Uri? _baseUri; private IStyle[]? _loaded; private bool _isLoading; @@ -31,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Styling /// The XAML service provider. public StyleInclude(IServiceProvider serviceProvider) { + _serviceProvider = serviceProvider; _baseUri = serviceProvider.GetContextBaseUri(); } @@ -52,7 +54,7 @@ namespace Avalonia.Markup.Xaml.Styling { _isLoading = true; var source = Source ?? throw new InvalidOperationException("StyleInclude.Source must be set."); - var loaded = (IStyle)AvaloniaXamlLoader.Load(source, _baseUri); + var loaded = (IStyle)AvaloniaXamlLoader.Load(_serviceProvider, source, _baseUri); _loaded = new[] { loaded }; _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index eec5d62fd3..2ca9a66fdc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -167,18 +167,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime #line hidden public static IServiceProvider CreateRootServiceProviderV2() { - return new RootServiceProvider(new NameScope()); + return new RootServiceProvider(new NameScope(), null); + } + public static IServiceProvider CreateRootServiceProviderV3(IServiceProvider parentServiceProvider) + { + return new RootServiceProvider(new NameScope(), parentServiceProvider); } #line default - class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider + class RootServiceProvider : IServiceProvider { private readonly INameScope _nameScope; + private readonly IServiceProvider _parentServiceProvider; private readonly IRuntimePlatform _runtimePlatform; - public RootServiceProvider(INameScope nameScope) + public RootServiceProvider(INameScope nameScope, IServiceProvider parentServiceProvider) { _nameScope = nameScope; + _parentServiceProvider = parentServiceProvider; _runtimePlatform = AvaloniaLocator.Current.GetService(); } @@ -187,19 +193,25 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime if (serviceType == typeof(INameScope)) return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) - return this; + return _parentServiceProvider?.GetService() + ?? DefaultAvaloniaXamlIlParentStackProvider.Instance; if (serviceType == typeof(IRuntimePlatform)) return _runtimePlatform ?? throw new KeyNotFoundException($"{nameof(IRuntimePlatform)} was not registered"); return null; } - public IEnumerable Parents + private class DefaultAvaloniaXamlIlParentStackProvider : IAvaloniaXamlIlParentStackProvider { - get + public static DefaultAvaloniaXamlIlParentStackProvider Instance { get; } = new(); + + public IEnumerable Parents { - if (Application.Current != null) - yield return Application.Current; + get + { + if (Application.Current != null) + yield return Application.Current; + } } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index bcb4bac457..a9528edc91 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -30,6 +30,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index 8eed5013a2..f148d95bf9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -2,6 +2,10 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Markup.Xaml.XamlIl.Runtime; +using Avalonia.Media; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Themes.Simple; using Avalonia.UnitTests; @@ -265,4 +269,43 @@ public class StyleIncludeTests Assert.IsType(control.Styles[0]); Assert.IsType(control.Styles[1]); } + + [Fact] + public void StyleInclude_From_CodeBehind_Resolves_Compiled() + { + using var locatorScope = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.BindToSelf(new AssetLoader(GetType().Assembly)); + + var sp = new TestServiceProvider(); + var styleInclude = new StyleInclude(sp) + { + Source = new Uri("avares://Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml") + }; + + var loaded = Assert.IsType(styleInclude.Loaded); + + Assert.Equal( + sp.GetService().Parents, + loaded.ServiceProvider.GetService().Parents); + } + + private class TestServiceProvider : IServiceProvider, IUriContext, IAvaloniaXamlIlParentStackProvider + { + private IServiceProvider _root = XamlIlRuntimeHelpers.CreateRootServiceProviderV2(); + public object GetService(Type serviceType) + { + if (serviceType == typeof(IUriContext)) + { + return this; + } + if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) + { + return this; + } + return _root.GetService(serviceType); + } + + public Uri BaseUri { get; set; } + public IEnumerable Parents { get; } = new[] { new ContentControl() }; + } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml new file mode 100644 index 0000000000..987c0f321a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml @@ -0,0 +1,5 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs new file mode 100644 index 0000000000..ceb122f05c --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml; + +public class StyleWithServiceLocator : Style +{ + public IServiceProvider ServiceProvider { get; } + + public StyleWithServiceLocator(IServiceProvider sp = null) + { + ServiceProvider = sp; + AvaloniaXamlLoader.Load(sp, this); + } +} From afa7c320282734392f04901be46106e3177d99e4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Nov 2022 23:27:37 -0500 Subject: [PATCH 17/78] Fix merge conflict --- ...niaXamlIlConstructorServiceProviderTransformer.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 0304165995..35e2624ff9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs @@ -41,18 +41,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public bool NeedsParentStack => true; public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { - if (_inheritContext) - { - codeGen.Ldloc(context.ContextLocal); - } - else - { - codeGen.Ldloc(context.ContextLocal); - var method = context.GetAvaloniaTypes().RuntimeHelpers - .FindMethod(m => m.Name == "CreateRootServiceProviderV3"); - codeGen.EmitCall(method); - } - codeGen.Ldloc(context.ContextLocal); return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } From 1caf79aa93158297629be9a82e3ef1140525ccd2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 1 Dec 2022 01:26:51 -0500 Subject: [PATCH 18/78] Fix build --- .../Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs index c81a3c1416..ed7bb98833 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -30,7 +30,8 @@ public class MergeResourceInclude : IResourceProvider if (_loaded == null) { _isLoading = true; - _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(_serviceProvider, Source, _baseUri); + var source = Source ?? throw new InvalidOperationException("MergeResourceInclude.Source must be set."); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri); _isLoading = false; } From 1af7eb6f3a93b5ed3b109abd9041409b041ebd9e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:17:07 +0000 Subject: [PATCH 19/78] fix window showdialog, and show raising exceptions when incorrect behavior is used. --- src/Avalonia.Controls/Window.cs | 99 ++++++++++--------- .../WindowTests.cs | 8 +- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1ff08f0169..6b936e495c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -641,47 +641,64 @@ namespace Avalonia.Controls } /// - /// Shows the window as a child of . + /// Shows the window as a child of . /// - /// Window that will be a parent of the shown window. + /// Window that will be the owner of the shown window. /// /// The window has already been closed. /// - public void Show(Window parent) + public void Show(Window owner) { - if (parent is null) + if (owner is null) { - throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent."); + throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent."); } - ShowCore(parent); + ShowCore(owner); } - private void ShowCore(Window? parent) + private void EnsureStateBeforeShow() + { + if (PlatformImpl == null) + { + throw new InvalidOperationException("Cannot re-show a closed window."); + } + + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } + } + + private void EnsureParentStateBeforeShow(Window owner) + { + if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + + if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + + if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible owner."); + } + } + + private void ShowCore(Window? owner) { try { IgnoreVisibilityChange = true; - - if (PlatformImpl == null) - { - throw new InvalidOperationException("Cannot re-show a closed window."); - } - if (parent != null) + EnsureStateBeforeShow(); + + if (owner != null) { - if (parent.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed parent."); - } - else if (parent == this) - { - throw new InvalidOperationException("A Window cannot be its own parent."); - } - else if (!parent.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + EnsureParentStateBeforeShow(owner); } if (_shown) @@ -707,15 +724,15 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); - if (PlatformImpl != null && parent?.PlatformImpl is not null) + if (PlatformImpl != null && owner?.PlatformImpl is not null) { - PlatformImpl.SetParent(parent.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl); } - Owner = parent; - parent?.AddChild(this, false); + Owner = owner; + owner?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(owner?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); @@ -758,26 +775,14 @@ namespace Avalonia.Controls { IgnoreVisibilityChange = true; + EnsureStateBeforeShow(); + if (owner == null) { throw new ArgumentNullException(nameof(owner)); } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + + EnsureParentStateBeforeShow(owner); RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); @@ -800,7 +805,7 @@ namespace Avalonia.Controls var result = new TaskCompletionSource(); - PlatformImpl?.SetParent(owner.PlatformImpl); + PlatformImpl?.SetParent(owner.PlatformImpl!); Owner = owner; owner.AddChild(this, true); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f73c3ac215..e544ca62bb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show a window with a closed parent.", ex.Message); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); } } @@ -431,7 +431,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -444,7 +444,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -456,7 +456,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(target)); - Assert.Equal("A Window cannot be its own parent.", ex.Message); + Assert.Equal("A Window cannot be its own owner.", ex.Message); } } From e7039842f5c38900a4dd3253207b99b95465c010 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:34:07 +0000 Subject: [PATCH 20/78] Show can be called multiple times but ignored. --- src/Avalonia.Controls/Window.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 6b936e495c..729bbaa4b3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -663,11 +663,6 @@ namespace Avalonia.Controls { throw new InvalidOperationException("Cannot re-show a closed window."); } - - if (_shown) - { - throw new InvalidOperationException("The window is already being shown."); - } } private void EnsureParentStateBeforeShow(Window owner) @@ -783,6 +778,11 @@ namespace Avalonia.Controls } EnsureParentStateBeforeShow(owner); + + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); From 6ed60610c9ddd823b523a0ccb485a2a31184d8a2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 2 Dec 2022 00:53:45 -0500 Subject: [PATCH 21/78] Add ServiceProvider to the xaml runtime loader too --- .../AvaloniaXamlIlRuntimeCompiler.cs | 20 ++++++----- .../AvaloniaXamlLoader.cs | 7 ++-- .../RuntimeXamlLoaderDocument.cs | 7 +++- .../Avalonia.Markup.Xaml.UnitTests.csproj | 2 +- .../Xaml/StyleIncludeTests.cs | 35 ++++++++++--------- ...tor.xaml => StyleWithServiceProvider.xaml} | 2 +- ...ml.cs => StyleWithServiceProvider.xaml.cs} | 4 +-- .../Xaml/XamlIlTests.cs | 21 +++++++++++ 8 files changed, 64 insertions(+), 34 deletions(-) rename tests/Avalonia.Markup.Xaml.UnitTests/Xaml/{StyleWithServiceLocator.xaml => StyleWithServiceProvider.xaml} (60%) rename tests/Avalonia.Markup.Xaml.UnitTests/Xaml/{StyleWithServiceLocator.xaml.cs => StyleWithServiceProvider.xaml.cs} (70%) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index 1ee4402481..28d5a55f86 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -209,7 +209,7 @@ namespace Avalonia.Markup.Xaml.XamlIl }; var parsedDocuments = new List(); - var rootInstances = new List(); + var originalDocuments = new List(); foreach (var document in documents) { @@ -235,7 +235,7 @@ namespace Avalonia.Markup.Xaml.XamlIl builder, compiler.DefinePopulateMethod(builder, parsed, AvaloniaXamlIlCompiler.PopulateName, true), compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true))); - rootInstances.Add(document.RootInstance); + originalDocuments.Add(document); } compiler.TransformGroup(parsedDocuments); @@ -251,7 +251,9 @@ namespace Avalonia.Markup.Xaml.XamlIl indexerClosureType.CreateTypeInfo(); trampolineBuilder.CreateTypeInfo(); - return createdTypes.Zip(rootInstances, (l, r) => (l, r)).Select(t => LoadOrPopulate(t.Item1, t.Item2)).ToArray(); + return createdTypes.Zip(originalDocuments, (l, r) => (l, r)) + .Select(t => LoadOrPopulate(t.Item1, t.Item2.RootInstance, t.Item2.ServiceProvider)) + .ToArray(); } static object LoadSreCore(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) @@ -260,7 +262,7 @@ namespace Avalonia.Markup.Xaml.XamlIl } #endif - static object LoadOrPopulate(Type created, object rootInstance) + static object LoadOrPopulate(Type created, object rootInstance, IServiceProvider parentServiceProvider) { var isp = Expression.Parameter(typeof(IServiceProvider)); @@ -271,6 +273,8 @@ namespace Avalonia.Markup.Xaml.XamlIl var populateCb = Expression.Lambda>( Expression.Call(populate, isp, Expression.Convert(epar, populate.GetParameters()[1].ParameterType)), isp, epar).Compile(); + + var serviceProvider = XamlIlRuntimeHelpers.CreateRootServiceProviderV3(parentServiceProvider); if (rootInstance == null) { @@ -282,7 +286,7 @@ namespace Avalonia.Markup.Xaml.XamlIl { overrideField.SetValue(null, new Action( - target => { populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), target); })); + target => { populateCb(serviceProvider, target); })); try { return Activator.CreateInstance(targetType); @@ -296,11 +300,11 @@ namespace Avalonia.Markup.Xaml.XamlIl var createCb = Expression.Lambda>( Expression.Convert(Expression.Call( created.GetMethod(AvaloniaXamlIlCompiler.BuildName), isp), typeof(object)), isp).Compile(); - return createCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2()); + return createCb(serviceProvider); } else { - populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), rootInstance); + populateCb(serviceProvider, rootInstance); return rootInstance; } } @@ -411,7 +415,7 @@ namespace Avalonia.Markup.Xaml.XamlIl var loaded = Assembly.LoadFile(asmPath) .GetTypes().First(x => x.Name == safeUri); _cecilGeneratedCache[safeUri] = loaded; - return LoadOrPopulate(loaded, rootInstance); + return LoadOrPopulate(loaded, rootInstance, null); } #endif } diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index b5d222d979..ffacc69f91 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -103,10 +103,9 @@ namespace Avalonia.Markup.Xaml var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); using (var stream = asset.stream) { - return runtimeLoader.Load(new RuntimeXamlLoaderDocument(absoluteUri, stream), new RuntimeXamlLoaderConfiguration - { - LocalAssembly = asset.assembly - }); + var document = new RuntimeXamlLoaderDocument(absoluteUri, stream) { ServiceProvider = sp }; + var configuration = new RuntimeXamlLoaderConfiguration { LocalAssembly = asset.assembly }; + return runtimeLoader.Load(document, configuration); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs index be22888156..80335bbf53 100644 --- a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs +++ b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs @@ -66,5 +66,10 @@ public class RuntimeXamlLoaderDocument /// /// The stream containing the XAML. /// - public Stream XamlStream { get; set; } + public Stream XamlStream { get; } + + /// + /// Parent's service provider to pass to the Build method or type ctor, if available. + /// + public IServiceProvider? ServiceProvider { get; set; } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index a9528edc91..6dce5eaab5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -30,7 +30,7 @@ - + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index f148d95bf9..d76e51f419 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -279,33 +279,34 @@ public class StyleIncludeTests var sp = new TestServiceProvider(); var styleInclude = new StyleInclude(sp) { - Source = new Uri("avares://Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml") + Source = new Uri("avares://Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml") }; - var loaded = Assert.IsType(styleInclude.Loaded); + var loaded = Assert.IsType(styleInclude.Loaded); Assert.Equal( sp.GetService().Parents, loaded.ServiceProvider.GetService().Parents); } +} - private class TestServiceProvider : IServiceProvider, IUriContext, IAvaloniaXamlIlParentStackProvider +public class TestServiceProvider : IServiceProvider, IUriContext, IAvaloniaXamlIlParentStackProvider +{ + private IServiceProvider _root = XamlIlRuntimeHelpers.CreateRootServiceProviderV2(); + public object GetService(Type serviceType) { - private IServiceProvider _root = XamlIlRuntimeHelpers.CreateRootServiceProviderV2(); - public object GetService(Type serviceType) + if (serviceType == typeof(IUriContext)) { - if (serviceType == typeof(IUriContext)) - { - return this; - } - if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) - { - return this; - } - return _root.GetService(serviceType); + return this; } - - public Uri BaseUri { get; set; } - public IEnumerable Parents { get; } = new[] { new ContentControl() }; + if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) + { + return this; + } + return _root.GetService(serviceType); } + + public Uri BaseUri { get; set; } + public List Parents { get; set; } = new List { new ContentControl() }; + IEnumerable IAvaloniaXamlIlParentStackProvider.Parents => Parents; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml similarity index 60% rename from tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml rename to tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml index 987c0f321a..664a26bae7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml @@ -1,5 +1,5 @@ diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml.cs similarity index 70% rename from tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs rename to tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml.cs index ceb122f05c..42da1604b9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceLocator.xaml.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleWithServiceProvider.xaml.cs @@ -4,11 +4,11 @@ using Avalonia.Styling; namespace Avalonia.Markup.Xaml.UnitTests.Xaml; -public class StyleWithServiceLocator : Style +public class StyleWithServiceProvider : Style { public IServiceProvider ServiceProvider { get; } - public StyleWithServiceLocator(IServiceProvider sp = null) + public StyleWithServiceProvider(IServiceProvider sp = null) { ServiceProvider = sp; AvaloniaXamlLoader.Load(sp, this); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 77a4932ccc..d945db0284 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -9,6 +9,7 @@ using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Markup.Xaml.UnitTests.Xaml; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; @@ -314,6 +315,26 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.NotNull(parsed.ItemTemplate); } + + [Fact] + public void Runtime_Loader_Should_Pass_Parents_From_ServiceProvider() + { + var sp = new TestServiceProvider + { + Parents = new List + { + new UserControl { Resources = { ["Resource1"] = new SolidColorBrush(Colors.Blue) } } + } + }; + var document = new RuntimeXamlLoaderDocument(@" +