From 8cfa7c175c4e9094d52448137db7731532a50c26 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Fri, 17 May 2019 15:58:36 +0200 Subject: [PATCH 01/30] Add blank cursor --- src/Avalonia.Input/Cursors.cs | 1 + src/Avalonia.X11/X11CursorFactory.cs | 22 ++++++++++++++++++++- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 7 ++++--- src/Gtk/Avalonia.Gtk3/GdkCursor.cs | 1 + src/Windows/Avalonia.Win32/CursorFactory.cs | 5 +++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 40b01117e3..c5566318a2 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -8,6 +8,8 @@ namespace Avalonia.X11 { class X11CursorFactory : IStandardCursorFactory { + private static IntPtr _nullCursor; + private readonly IntPtr _display; private Dictionary _cursors; @@ -42,16 +44,34 @@ namespace Avalonia.X11 public X11CursorFactory(IntPtr display) { _display = display; + _nullCursor = GetNullCursor(display); _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } public IPlatformHandle GetCursor(StandardCursorType cursorType) { - var handle = s_mapping.TryGetValue(cursorType, out var shape) + IntPtr handle; + if (cursorType == StandardCursorType.None) + { + handle = _nullCursor; + } + else + { + handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; + } return new PlatformHandle(handle, "XCURSOR"); } + + private static IntPtr GetNullCursor(IntPtr display) + { + XColor color = new XColor(); + byte[] data = new byte[] { 0 }; + IntPtr window = XLib.XRootWindow(display, 0); + IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index a28b1cbb1a..f36e6b986d 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -12,7 +12,8 @@ namespace Avalonia.Gtk3 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.None, CursorType.Blank}, + { StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -36,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = @@ -80,4 +81,4 @@ namespace Avalonia.Gtk3 return rv; } } -} \ No newline at end of file +} diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs index 4fad8208b3..aa0f8cde0d 100644 --- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs +++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs @@ -2,6 +2,7 @@ { enum GdkCursorType { + Blank = -2, CursorIsPixmap = -1, XCursor = 0, Arrow = 2, diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index e582b5fb82..df413addef 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -41,7 +41,8 @@ namespace Avalonia.Win32 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, 32650}, + {StandardCursorType.None, 0}, + { StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -69,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From 4ad1acd24eeb836ff3b4fb06cb83011ed635d1e0 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:05:49 +0200 Subject: [PATCH 02/30] Fix X11 NullCursor --- src/Avalonia.X11/X11CursorFactory.cs | 8 ++++---- src/Avalonia.X11/XLib.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c5566318a2..c020c44662 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -51,7 +51,7 @@ namespace Avalonia.X11 public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -66,12 +66,12 @@ namespace Avalonia.X11 } private static IntPtr GetNullCursor(IntPtr display) - { + { XColor color = new XColor(); byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); - IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a146f922d..3c41f7bdde 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -321,6 +321,9 @@ namespace Avalonia.X11 public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask, ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot); + [DllImport(libX11)] + public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height); + [DllImport(libX11)] public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width, int height, IntPtr fg, IntPtr bg, int depth); From 3860fafa000e9127f436745b998f6d8994cdb715 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:10:49 +0200 Subject: [PATCH 03/30] Fix formatting --- src/Avalonia.X11/X11CursorFactory.cs | 10 +++++----- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 4 ++-- src/Windows/Avalonia.Win32/CursorFactory.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c020c44662..0a8b1ee9c4 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11 class X11CursorFactory : IStandardCursorFactory { private static IntPtr _nullCursor; - + private readonly IntPtr _display; private Dictionary _cursors; @@ -48,10 +48,10 @@ namespace Avalonia.X11 _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -61,7 +61,7 @@ namespace Avalonia.X11 handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; - } + } return new PlatformHandle(handle, "XCURSOR"); } @@ -71,7 +71,7 @@ namespace Avalonia.X11 byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index f36e6b986d..95fa3ba9e3 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -13,7 +13,7 @@ namespace Avalonia.Gtk3 { {StandardCursorType.None, CursorType.Blank}, - { StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -37,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index df413addef..f1fd74f931 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -42,7 +42,7 @@ namespace Avalonia.Win32 { {StandardCursorType.None, 0}, - { StandardCursorType.AppStarting, 32650}, + {StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -70,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From c3f142af2e75c83c57262e9ab27efa7c25e02043 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 18:35:47 +0300 Subject: [PATCH 04/30] Add ViewModelViewHost and TransitioningUserControl --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 143 +++--------------- .../TransitioningUserControl.cs | 127 ++++++++++++++++ src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 75 +++++++++ 3 files changed, 219 insertions(+), 126 deletions(-) create mode 100644 src/Avalonia.ReactiveUI/TransitioningUserControl.cs create mode 100644 src/Avalonia.ReactiveUI/ViewModelViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..bf295aafca 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningUserControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, value); } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs new file mode 100644 index 0000000000..fb5258f2e4 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs @@ -0,0 +1,127 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningUserControl : UserControl + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when content disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the content with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); + base.Content = content; + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..84108a9b52 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningUserControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Updates the Content when ViewModel changes. + /// + /// Property changed event arguments. + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); + base.OnPropertyChanged(e); + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file From 97b44c02d25d07c0e2adfecb842e016c54d2e434 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:09:51 +0300 Subject: [PATCH 05/30] Add a unit test for ViewModelViewHost --- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 27 ++++--- .../Attributes.cs | 6 ++ .../AvaloniaActivationForViewFetcherTest.cs | 2 +- .../RoutedViewHostTest.cs | 2 +- .../ViewModelViewHostTest.cs | 72 +++++++++++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 84108a9b52..6f80bff4b8 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Reactive.Disposables; using ReactiveUI; using Splat; @@ -18,7 +20,20 @@ namespace Avalonia.ReactiveUI /// public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); - + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + /// /// Gets or sets the ViewModel to display. /// @@ -33,16 +48,6 @@ namespace Avalonia.ReactiveUI /// public IViewLocator ViewLocator { get; set; } - /// - /// Updates the Content when ViewModel changes. - /// - /// Property changed event arguments. - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); - base.OnPropertyChanged(e); - } - /// /// Invoked when ReactiveUI router navigates to a view model. /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs new file mode 100644 index 0000000000..79e58f63e9 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -0,0 +1,6 @@ +using Xunit; + +// Required to avoid InvalidOperationException sometimes thrown +// from Splat.MemoizingMRUCache.cs which is not thread-safe. +// Thrown when trying to access WhenActivated concurrently. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index d9f1ce47dd..f4dffdc0c3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -13,7 +13,7 @@ using Splat; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class AvaloniaActivationForViewFetcherTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index 401d169896..c6017d3f5f 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; using System.Reactive; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class RoutedViewHostTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs new file mode 100644 index 0000000000..5d5d15358e --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ViewModelViewHostTest + { + public class FirstViewModel { } + + public class FirstView : ReactiveUserControl { } + + public class SecondViewModel : ReactiveObject { } + + public class SecondView : ReactiveUserControl { } + + public ViewModelViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor)); + } + + [Fact] + public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel() + { + var defaultContent = new TextBlock(); + var host = new ViewModelViewHost + { + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstViewModel(); + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + + var second = new SecondViewModel(); + host.ViewModel = second; + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondView), host.Content.GetType()); + Assert.Equal(second, ((SecondView)host.Content).DataContext); + Assert.Equal(second, ((SecondView)host.Content).ViewModel); + + host.ViewModel = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + } + } +} \ No newline at end of file From 724a5da8963ea83d616fce8691a9ded211ccb4ef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:10:40 +0300 Subject: [PATCH 06/30] Add license headers --- tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs | 3 +++ .../AvaloniaActivationForViewFetcherTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs index 79e58f63e9..a79647d355 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Xunit; // Required to avoid InvalidOperationException sometimes thrown diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index f4dffdc0c3..2c81e8fea3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index c6017d3f5f..e873c60e36 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 5d5d15358e..a21bf34ef5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Controls; using Avalonia.UnitTests; using ReactiveUI; From e8c0012c318522529f0e572a7535ab60d628d96e Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:26:29 +0300 Subject: [PATCH 07/30] Unit tests for ReactiveWindow and ReactiveUserControl --- .../ReactiveUserControlTest.cs | 34 +++++++++++++++++ .../ReactiveWindowTest.cs | 37 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs new file mode 100644 index 0000000000..328b749ba1 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveUserControlTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleView : ReactiveUserControl { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() + { + var view = new ExampleView(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs new file mode 100644 index 0000000000..ff77de8d28 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveWindowTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleWindow : ReactiveWindow { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var view = new ExampleWindow(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } + } +} \ No newline at end of file From fe7e7054fb7012b8f901de4d7abc8664bc9f40e9 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 20:40:26 +0300 Subject: [PATCH 08/30] Add initial AutoDataTemplateBindingHook implementation --- .../AutoDataTemplateBindingHook.cs | 60 +++++++++++++++++++ .../AutoDataTemplateBindingHookTest.cs | 56 +++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..bd156a5884 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace Avalonia.ReactiveUI +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + { + var template = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + return (DataTemplate)loader.Load(template); + }); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + if (lastViewProperty == null) + return true; + + var itemsControl = lastViewProperty.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate.Value; + return true; + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs new file mode 100644 index 0000000000..2391daece2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -0,0 +1,56 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Xunit; +using ReactiveUI; +using Avalonia.ReactiveUI; +using Avalonia.UnitTests; +using Avalonia.Controls; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Splat; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class AutoDataTemplateBindingHookTest + { + public class NestedViewModel : ReactiveObject { } + + public class NestedView : ReactiveUserControl { } + + public class ExampleViewModel : ReactiveObject + { + public ObservableCollection Items { get; } = new ObservableCollection(); + } + + public class ExampleView : ReactiveUserControl + { + public ListBox List { get; } = new ListBox(); + + public ExampleView() + { + Content = List; + ViewModel = new ExampleViewModel(); + this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items); + } + } + + public AutoDataTemplateBindingHookTest() + { + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + } + + [Fact] + public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() + { + var view = new ExampleView(); + var root = new TestRoot + { + Child = view + }; + Assert.NotNull(view.List.ItemTemplate); + } + } +} \ No newline at end of file From b486112d296354f0d9d46f4fea72ea8bdb5c2bef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:06:31 +0300 Subject: [PATCH 09/30] Use null propagation --- src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bd156a5884..bb660fd76a 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -38,10 +38,7 @@ namespace Avalonia.ReactiveUI { var viewProperties = getCurrentViewProperties(); var lastViewProperty = viewProperties.LastOrDefault(); - if (lastViewProperty == null) - return true; - - var itemsControl = lastViewProperty.Sender as ItemsControl; + var itemsControl = lastViewProperty?.Sender as ItemsControl; if (itemsControl == null) return true; From 72fe95272a09644f72fd34c7d14acf42ef576819 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:08:52 +0300 Subject: [PATCH 10/30] Register auto data template binding hook --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index f67cb7f40a..ced26a3004 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.Register( - () => new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); }); } } From d2574b383a1880bdd3aebfc94b9ee26ce653629d Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:47:18 +0300 Subject: [PATCH 11/30] Use FuncDataTemplate --- .../AutoDataTemplateBindingHook.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bb660fd76a..d7cfebf1d8 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Templates; using ReactiveUI; @@ -14,19 +16,18 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => { - var template = @" - - -"; - - var loader = new AvaloniaXamlLoader(); - return (DataTemplate)loader.Load(template); + return new FuncDataTemplate(x => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); }); /// From 65f5f58a07f29c5baf4fe80218cc2a83deba5dc6 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 00:36:53 +0300 Subject: [PATCH 12/30] ViewModelViewHost type resolution assertion --- .../AutoDataTemplateBindingHookTest.cs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 2391daece2..4ff3e1fed6 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -6,9 +6,12 @@ using ReactiveUI; using Avalonia.ReactiveUI; using Avalonia.UnitTests; using Avalonia.Controls; +using Avalonia.Controls.Templates; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.VisualTree; +using Avalonia.Controls.Presenters; using Splat; namespace Avalonia.ReactiveUI.UnitTests @@ -26,7 +29,7 @@ namespace Avalonia.ReactiveUI.UnitTests public class ExampleView : ReactiveUserControl { - public ListBox List { get; } = new ListBox(); + public ItemsControl List { get; } = new ItemsControl(); public ExampleView() { @@ -40,17 +43,48 @@ namespace Avalonia.ReactiveUI.UnitTests { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); } [Fact] public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() { var view = new ExampleView(); - var root = new TestRoot - { - Child = view - }; Assert.NotNull(view.List.ItemTemplate); } + + [Fact] + public void Should_Use_View_Model_View_Host_As_Data_Template() + { + var view = new ExampleView(); + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + Assert.IsType(container.Child); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + MemberSelector = parent.MemberSelector, + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + } + }; + }); + } } } \ No newline at end of file From 8a2d1662fdcd92057d39aa39a2e8296e134ab0bd Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 01:20:36 +0300 Subject: [PATCH 13/30] Remove LazyT, test new template resolver --- .../AutoDataTemplateBindingHook.cs | 23 +++++++-------- .../AutoDataTemplateBindingHookTest.cs | 28 ++++++++++++++++++- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index d7cfebf1d8..3f41f54363 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -16,19 +16,16 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => { - return new FuncDataTemplate(x => - { - var control = new ViewModelViewHost(); - var context = control.GetObservable(Control.DataContextProperty); - control.Bind(ViewModelViewHost.ViewModelProperty, context); - control.HorizontalContentAlignment = HorizontalAlignment.Stretch; - control.VerticalContentAlignment = VerticalAlignment.Stretch; - return control; - }, - true); - }); + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); /// public bool ExecuteHook( @@ -51,7 +48,7 @@ namespace Avalonia.ReactiveUI if (itemsControl.ItemTemplate != null) return true; - itemsControl.ItemTemplate = DefaultItemTemplate.Value; + itemsControl.ItemTemplate = DefaultItemTemplate; return true; } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 4ff3e1fed6..667462eb91 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -13,6 +13,8 @@ using System.Linq; using Avalonia.VisualTree; using Avalonia.Controls.Presenters; using Splat; +using System.Threading.Tasks; +using System; namespace Avalonia.ReactiveUI.UnitTests { @@ -42,8 +44,8 @@ namespace Avalonia.ReactiveUI.UnitTests public AutoDataTemplateBindingHookTest() { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor)); } [Fact] @@ -70,6 +72,30 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.IsType(container.Child); } + [Fact] + public void Should_Resolve_And_Embedd_Appropriate_View_Model() + { + var view = new ExampleView(); + var root = new TestRoot { Child = view }; + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + var host = (ViewModelViewHost) container.Child; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + + host.DataContext = "changed context"; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate(parent => From 6360de1921e157fde03f6e9c1d25e4094ec3fcb7 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 29 May 2019 01:28:37 +0300 Subject: [PATCH 14/30] Derive transitioning control from ContentControl --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 2 +- ...trol.cs => TransitioningContentControl.cs} | 14 ++-- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 2 +- .../TransitioningContentControlTest.cs | 64 +++++++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) rename src/Avalonia.ReactiveUI/{TransitioningUserControl.cs => TransitioningContentControl.cs} (87%) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index bf295aafca..05edeea683 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,7 +53,7 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : TransitioningUserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger { /// /// for the property. diff --git a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs similarity index 87% rename from src/Avalonia.ReactiveUI/TransitioningUserControl.cs rename to src/Avalonia.ReactiveUI/TransitioningContentControl.cs index fb5258f2e4..db8f3c964c 100644 --- a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -11,27 +11,27 @@ namespace Avalonia.ReactiveUI /// /// A ContentControl that animates the transition when its content is changed. /// - public class TransitioningUserControl : UserControl + public class TransitioningContentControl : ContentControl, IStyleable { /// /// for the property. /// public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), + AvaloniaProperty.Register(nameof(DefaultContent), CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); /// /// for the property. /// public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), + AvaloniaProperty.Register(nameof(DefaultContent), CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// for the property. /// public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); + AvaloniaProperty.Register(nameof(DefaultContent)); /// /// Gets or sets the animation played when content appears. @@ -68,6 +68,12 @@ namespace Avalonia.ReactiveUI get => base.Content; set => UpdateContentWithTransition(value); } + + /// + /// TransitioningContentControl uses the default ContentControl + /// template from Avalonia default theme. + /// + Type IStyleable.StyleKey => typeof(ContentControl); /// /// Updates the content with transitions. diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 6f80bff4b8..5cfa464c37 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -13,7 +13,7 @@ namespace Avalonia.ReactiveUI /// the ViewModel property and display it. This control is very useful /// inside a DataTemplate to display the View associated with a ViewModel. /// - public class ViewModelViewHost : TransitioningUserControl, IViewFor, IEnableLogger + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger { /// /// for the property. diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs new file mode 100644 index 0000000000..5ffcede5dd --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class TransitioningContentControlTest + { + [Fact] + public void Transitioning_Control_Should_Derive_Template_From_Content_Control() + { + var target = new TransitioningContentControl(); + var stylable = (IStyledElement)target; + Assert.Equal(typeof(ContentControl),stylable.StyleKey); + } + + [Fact] + public void Transitioning_Control_Template_Should_Be_Instantiated() + { + var target = new TransitioningContentControl + { + FadeInAnimation = null, + FadeOutAnimation = null, + Template = GetTemplate(), + Content = "Foo" + }; + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + var child = ((IVisual)target).VisualChildren.Single(); + Assert.IsType(child); + child = child.VisualChildren.Single(); + Assert.IsType(child); + child = child.VisualChildren.Single(); + Assert.IsType(child); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], + [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], + } + }; + }); + } + } +} \ No newline at end of file From f0574cbf1a570f6530eb0349967ce5740dccf9ef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 29 May 2019 02:10:03 +0300 Subject: [PATCH 15/30] Use PageTransition, fix possible bug with CrossFade --- .../TransitioningContentControl.cs | 82 +++---------------- src/Avalonia.Visuals/Animation/CrossFade.cs | 8 +- .../RoutedViewHostTest.cs | 3 +- .../TransitioningContentControlTest.cs | 3 +- .../ViewModelViewHostTest.cs | 3 +- 5 files changed, 19 insertions(+), 80 deletions(-) diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index db8f3c964c..68d69bc95f 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -14,18 +14,11 @@ namespace Avalonia.ReactiveUI public class TransitioningContentControl : ContentControl, IStyleable { /// - /// for the property. + /// for the property. /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// for the property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + public static readonly AvaloniaProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + new CrossFade(TimeSpan.FromSeconds(0.5))); /// /// for the property. @@ -34,23 +27,14 @@ namespace Avalonia.ReactiveUI AvaloniaProperty.Register(nameof(DefaultContent)); /// - /// Gets or sets the animation played when content appears. + /// Gets or sets the animation played when content appears and disappears. /// - public IAnimation FadeInAnimation + public IPageTransition PageTransition { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); } - /// - /// Gets or sets the animation played when content disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - /// /// Gets or sets the content displayed whenever there is no page currently routed. /// @@ -81,53 +65,11 @@ namespace Avalonia.ReactiveUI /// New content to set. private async void UpdateContentWithTransition(object content) { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); + if (PageTransition != null) + await PageTransition.Start(this, null, true); base.Content = content; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + if (PageTransition != null) + await PageTransition.Start(null, this, true); } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 6b8cb8b755..614d828259 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -14,8 +14,8 @@ namespace Avalonia.Animation /// public class CrossFade : IPageTransition { - private Animation _fadeOutAnimation; - private Animation _fadeInAnimation; + private readonly Animation _fadeOutAnimation; + private readonly Animation _fadeInAnimation; /// /// Initializes a new instance of the class. @@ -61,10 +61,10 @@ namespace Avalonia.Animation new Setter { Property = Visual.OpacityProperty, - Value = 0d + Value = 1d } }, - Cue = new Cue(0d) + Cue = new Cue(1d) } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index e873c60e36..8c5b5083e8 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -62,8 +62,7 @@ namespace Avalonia.ReactiveUI.UnitTests { Router = screen.Router, DefaultContent = defaultContent, - FadeOutAnimation = null, - FadeInAnimation = null + PageTransition = null }; var root = new TestRoot diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs index 5ffcede5dd..f09eea5ec5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs @@ -28,8 +28,7 @@ namespace Avalonia.ReactiveUI.UnitTests { var target = new TransitioningContentControl { - FadeInAnimation = null, - FadeOutAnimation = null, + PageTransition = null, Template = GetTemplate(), Content = "Foo" }; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index a21bf34ef5..8bed5adcd4 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -33,8 +33,7 @@ namespace Avalonia.ReactiveUI.UnitTests var host = new ViewModelViewHost { DefaultContent = defaultContent, - FadeOutAnimation = null, - FadeInAnimation = null + PageTransition = null }; var root = new TestRoot From 12ebd106cbed16c77c64d53b16def9bc929fa7dc Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 29 May 2019 02:12:03 +0300 Subject: [PATCH 16/30] Typo fix --- src/Avalonia.ReactiveUI/TransitioningContentControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 68d69bc95f..1bec5fc365 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -17,7 +17,7 @@ namespace Avalonia.ReactiveUI /// for the property. /// public static readonly AvaloniaProperty PageTransitionProperty = - AvaloniaProperty.Register(nameof(DefaultContent), + AvaloniaProperty.Register(nameof(PageTransition), new CrossFade(TimeSpan.FromSeconds(0.5))); /// From 2b6fdc153148c2687e41cb1fd4256d5011db3b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 30 May 2019 20:49:39 +0200 Subject: [PATCH 17/30] Initial port of WPF WrapPanel --- src/Avalonia.Controls/WrapPanel.cs | 195 ++++++++++++++--------------- 1 file changed, 93 insertions(+), 102 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 597734d400..b6215a28cc 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -92,109 +92,127 @@ namespace Avalonia.Controls } } - private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size); - - private UVSize CreateUVSize() => new UVSize(Orientation); - /// - protected override Size MeasureOverride(Size availableSize) + protected override Size MeasureOverride(Size constraint) { - var desiredSize = CreateUVSize(); - var lineSize = CreateUVSize(); - var uvAvailableSize = CreateUVSize(availableSize); + var curLineSize = new UVSize(Orientation); + var panelSize = new UVSize(Orientation); + var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height); - foreach (var child in Children) + var childConstraint = new Size(constraint.Width, constraint.Height); + + for (int i = 0, count = Children.Count; i < count; i++) { - child.Measure(availableSize); - var childSize = CreateUVSize(child.DesiredSize); - if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line + var child = Children[i]; + if (child == null) continue; + + //Flow passes its own constrint to children + child.Measure(childConstraint); + + //this is the size of the child in UV space + var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + + if ((curLineSize.U + sz.U) > uvConstraint.U) //need to switch to another line { - lineSize.U += childSize.U; - lineSize.V = Max(lineSize.V, childSize.V); + panelSize.U = Max(curLineSize.U, panelSize.U); + panelSize.V += curLineSize.V; + curLineSize = sz; + + if (sz.U > uvConstraint.U) //the element is wider then the constrint - give it a separate line + { + panelSize.U = Max(sz.U, panelSize.U); + panelSize.V += sz.V; + curLineSize = new UVSize(Orientation); + } } - else // moving to next line + else //continue to accumulate a line { - desiredSize.U = Max(lineSize.U, uvAvailableSize.U); - desiredSize.V += lineSize.V; - lineSize = childSize; + curLineSize.U += sz.U; + curLineSize.V = Max(sz.V, curLineSize.V); } } - // last element - desiredSize.U = Max(lineSize.U, desiredSize.U); - desiredSize.V += lineSize.V; - return desiredSize.ToSize(); + //the last line size, if any should be added + panelSize.U = Max(curLineSize.U, panelSize.U); + panelSize.V += curLineSize.V; + + //go from UV space to W/H space + return new Size(panelSize.Width, panelSize.Height); } /// protected override Size ArrangeOverride(Size finalSize) { + int firstInLine = 0; double accumulatedV = 0; - var uvFinalSize = CreateUVSize(finalSize); - var lineSize = CreateUVSize(); - int firstChildInLineIndex = 0; - for (int index = 0; index < Children.Count; index++) + UVSize curLineSize = new UVSize(Orientation); + UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height); + + for (int i = 0; i < Children.Count; i++) { - var child = Children[index]; - var childSize = CreateUVSize(child.DesiredSize); - if (lineSize.U + childSize.U <= uvFinalSize.U) // same line + var child = Children[i]; + if (child == null) continue; + + var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + + if ((curLineSize.U + sz.U) > uvFinalSize.U) //need to switch to another line { - lineSize.U += childSize.U; - lineSize.V = Max(lineSize.V, childSize.V); + arrangeLine(accumulatedV, curLineSize.V, firstInLine, i); + + accumulatedV += curLineSize.V; + curLineSize = sz; + + if (sz.U > uvFinalSize.U) //the element is wider then the constraint - give it a separate line + { + //switch to next line which only contain one element + arrangeLine(accumulatedV, sz.V, i, ++i); + + accumulatedV += sz.V; + curLineSize = new UVSize(Orientation); + } + firstInLine = i; } - else // moving to next line + else //continue to accumulate a line { - var controlsInLine = GetControlsBetween(firstChildInLineIndex, index); - ArrangeLine(accumulatedV, lineSize.V, controlsInLine); - accumulatedV += lineSize.V; - lineSize = childSize; - firstChildInLineIndex = index; + curLineSize.U += sz.U; + curLineSize.V = Max(sz.V, curLineSize.V); } } - if (firstChildInLineIndex < Children.Count) + //arrange the last line, if any + if (firstInLine < Children.Count) { - var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count); - ArrangeLine(accumulatedV, lineSize.V, controlsInLine); + arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count); } - return finalSize; - } - private IEnumerable GetControlsBetween(int first, int last) - { - return Children.Skip(first).Take(last - first); + return finalSize; } - private void ArrangeLine(double v, double lineV, IEnumerable controls) + private void arrangeLine(double v, double lineV, int start, int end) { double u = 0; bool isHorizontal = (Orientation == Orientation.Horizontal); - foreach (var child in controls) + + for (int i = 0, count = Children.Count; i < count; i++) { - var childSize = CreateUVSize(child.DesiredSize); - var x = isHorizontal ? u : v; - var y = isHorizontal ? v : u; - var width = isHorizontal ? childSize.U : lineV; - var height = isHorizontal ? lineV : childSize.U; - child.Arrange(new Rect(x, y, width, height)); - u += childSize.U; + var child = Children[i]; + if (child != null) + { + UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + double layoutSlotU = childSize.U; + child.Arrange(new Rect( + (isHorizontal ? u : v), + (isHorizontal ? v : u), + (isHorizontal ? layoutSlotU : lineV), + (isHorizontal ? lineV : layoutSlotU))); + u += layoutSlotU; + } } } - /// - /// Used to not not write separate code for horizontal and vertical orientation. - /// U is direction in line. (x if orientation is horizontal) - /// V is direction of lines. (y if orientation is horizontal) - /// - [DebuggerDisplay("U = {U} V = {V}")] + private struct UVSize { - private readonly Orientation _orientation; - - internal double U; - - internal double V; - - private UVSize(Orientation orientation, double width, double height) + internal UVSize(Orientation orientation, double width, double height) { U = V = 0d; _orientation = orientation; @@ -202,52 +220,25 @@ namespace Avalonia.Controls Height = height; } - internal UVSize(Orientation orientation, Size size) - : this(orientation, size.Width, size.Height) - { - } - internal UVSize(Orientation orientation) { U = V = 0d; _orientation = orientation; } - private double Width + internal double U; + internal double V; + private Orientation _orientation; + + internal double Width { get { return (_orientation == Orientation.Horizontal ? U : V); } - set - { - if (_orientation == Orientation.Horizontal) - { - U = value; - } - else - { - V = value; - } - } + set { if (_orientation == Orientation.Horizontal) U = value; else V = value; } } - - private double Height + internal double Height { get { return (_orientation == Orientation.Horizontal ? V : U); } - set - { - if (_orientation == Orientation.Horizontal) - { - V = value; - } - else - { - U = value; - } - } - } - - public Size ToSize() - { - return new Size(Width, Height); + set { if (_orientation == Orientation.Horizontal) V = value; else U = value; } } } } From f0ada1c82ff27fa72c512624bb840dbee83c6b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 30 May 2019 21:18:34 +0200 Subject: [PATCH 18/30] Update MathUtilities.cs --- src/Avalonia.Base/Utilities/MathUtilities.cs | 118 ++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index dcb3ef4a2b..546133bb03 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,6 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Runtime.InteropServices; + namespace Avalonia.Utilities { /// @@ -8,6 +11,86 @@ namespace Avalonia.Utilities /// public static class MathUtilities { + /// + /// AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// LessThan - Returns whether or not the first double is less than the second double. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. + /// The first double to compare. + /// The second double to compare. + public static bool LessThan(double value1, double value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + + /// + /// GreaterThan - Returns whether or not the first double is greater than the second double. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThan(double value1, double value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + + /// + /// LessThanOrClose - Returns whether or not the first double is less than or close to + /// the second double. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. + /// The first double to compare. + /// The second double to compare. + public static bool LessThanOrClose(double value1, double value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + + /// + /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to + /// the second double. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThanOrClose(double value1, double value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + + /// + /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// The double to compare to 1. + public static bool IsOne(double value) + { + return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// The double to compare to 0. + public static bool IsZero(double value) + { + return Math.Abs(value) < 10.0 * double.Epsilon; + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -31,6 +114,39 @@ namespace Avalonia.Utilities } } + /// + /// Calculates the value to be used for layout rounding at high DPI. + /// + /// Input value to be rounded. + /// Ratio of screen's DPI to layout DPI + /// Adjusted value that will produce layout rounding on screen at high dpi. + /// This is a layout helper method. It takes DPI into account and also does not return + /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with + /// UseLayoutRounding property and should not be used as a general rounding utility. + public static double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!MathUtilities.AreClose(dpiScale, 1.0)) + { + newValue = Math.Round(value * dpiScale) / dpiScale; + // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. + if (double.IsNaN(newValue) || + double.IsInfinity(newValue) || + MathUtilities.AreClose(newValue, double.MaxValue)) + { + newValue = value; + } + } + else + { + newValue = Math.Round(value); + } + + return newValue; + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -54,4 +170,4 @@ namespace Avalonia.Utilities } } } -} +} \ No newline at end of file From 10d295decf53bec90de96c7663fdfcfc72adcaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 30 May 2019 21:18:42 +0200 Subject: [PATCH 19/30] Use MathUtilities --- src/Avalonia.Controls/WrapPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index b6215a28cc..86f1691e70 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -112,13 +112,13 @@ namespace Avalonia.Controls //this is the size of the child in UV space var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); - if ((curLineSize.U + sz.U) > uvConstraint.U) //need to switch to another line + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U) //need to switch to another line { panelSize.U = Max(curLineSize.U, panelSize.U); panelSize.V += curLineSize.V; curLineSize = sz; - if (sz.U > uvConstraint.U) //the element is wider then the constrint - give it a separate line + if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line { panelSize.U = Max(sz.U, panelSize.U); panelSize.V += sz.V; From 6f652867ff047631c452d685178645759eccc528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 30 May 2019 21:25:30 +0200 Subject: [PATCH 20/30] Fix --- src/Avalonia.Controls/WrapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 86f1691e70..fb6eecbef1 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls //this is the size of the child in UV space var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); - if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U) //need to switch to another line + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line { panelSize.U = Max(curLineSize.U, panelSize.U); panelSize.V += curLineSize.V; From 9844b05bfdd23e90c4fc46ef960a14d7e977e7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 30 May 2019 21:27:23 +0200 Subject: [PATCH 21/30] Add missing using --- src/Avalonia.Controls/WrapPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index fb6eecbef1..2382e689ad 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Avalonia.Input; +using Avalonia.Utilities; using static System.Math; From 62b8c679ea1913c2eae2c3c2c8f534ff2ac76d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Thu, 30 May 2019 21:48:48 +0200 Subject: [PATCH 22/30] Update WrapPanel.cs --- src/Avalonia.Controls/WrapPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 2382e689ad..fcc21a7b41 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -156,14 +156,14 @@ namespace Avalonia.Controls var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); - if ((curLineSize.U + sz.U) > uvFinalSize.U) //need to switch to another line + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line { arrangeLine(accumulatedV, curLineSize.V, firstInLine, i); accumulatedV += curLineSize.V; curLineSize = sz; - if (sz.U > uvFinalSize.U) //the element is wider then the constraint - give it a separate line + if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line { //switch to next line which only contain one element arrangeLine(accumulatedV, sz.V, i, ++i); From c3c7e8d286fc157d059bb7744ed9c7aa6fedc066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 31 May 2019 17:53:08 +0200 Subject: [PATCH 23/30] Fix copy and paste error --- src/Avalonia.Controls/WrapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index fcc21a7b41..4df1b39400 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -194,7 +194,7 @@ namespace Avalonia.Controls double u = 0; bool isHorizontal = (Orientation == Orientation.Horizontal); - for (int i = 0, count = Children.Count; i < count; i++) + for (int i = start; i < end; i++) { var child = Children[i]; if (child != null) From 36d24bcea207254b1323670dd333ab3ba64a4218 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 31 May 2019 17:15:25 +0100 Subject: [PATCH 24/30] improved notification colours. --- src/Avalonia.Themes.Default/Accents/BaseDark.xaml | 10 +++++----- src/Avalonia.Themes.Default/Accents/BaseLight.xaml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index f84e09510b..8f7d56dbc6 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1,1,1,1 0.5 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 18c32b02bc..666596d710 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1 0.5 From bf8e23457172aeb73d4d54aea8ed711ddc200cd9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 31 May 2019 17:36:04 +0100 Subject: [PATCH 25/30] allow notification card size to be controlled with styles. --- src/Avalonia.Themes.Default/NotificationCard.xaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/NotificationCard.xaml b/src/Avalonia.Themes.Default/NotificationCard.xaml index e94cb33d1e..bcc7bb9a29 100644 --- a/src/Avalonia.Themes.Default/NotificationCard.xaml +++ b/src/Avalonia.Themes.Default/NotificationCard.xaml @@ -13,7 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="8,8,0,0"> - + @@ -40,6 +40,10 @@ + +