diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index e10de93530..85e7a1f34d 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index a217a8272d..1ee4aa56a2 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 54bfe6e38a..f51c693777 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -31,6 +31,7 @@ ComPtr _parent; bool _closed; bool _isEnabled; + bool _canBecomeKeyWindow; bool _isExtended; AvnMenu* _menu; } @@ -216,29 +217,38 @@ -(BOOL)canBecomeKeyWindow { - // If the window has a child window being shown as a dialog then don't allow it to become the key window. - for(NSWindow* uch in [self childWindows]) + if(_canBecomeKeyWindow) { - if (![uch conformsToProtocol:@protocol(AvnWindowProtocol)]) + // If the window has a child window being shown as a dialog then don't allow it to become the key window. + for(NSWindow* uch in [self childWindows]) { - continue; - } + if (![uch conformsToProtocol:@protocol(AvnWindowProtocol)]) + { + continue; + } - id ch = (id ) uch; + id ch = (id ) uch; - return !ch.isDialog; - } + if(ch.isDialog) + return false; + } - return true; + return true; + } + + return false; } +#ifndef IS_NSPANEL -(BOOL)canBecomeMainWindow { -#ifdef IS_NSPANEL - return false; -#else return true; +} #endif + +-(void)setCanBecomeKeyWindow:(bool)value +{ + _canBecomeKeyWindow = value; } -(bool)shouldTryToHandleEvents diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index cb52047148..3c5afd9424 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -26,13 +26,17 @@ private: PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { WindowEvents = events; - [Window setLevel:NSPopUpMenuWindowLevel]; } protected: virtual NSWindowStyleMask GetStyle() override { return NSWindowStyleMaskBorderless; } + + virtual void OnInitialiseNSWindow () override + { + [Window setLevel:NSPopUpMenuWindowLevel]; + } public: virtual bool ShouldTakeFocusOnShow() override @@ -54,4 +58,4 @@ extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) IAvnPopup* ptr = dynamic_cast(new PopupImpl(events, gl)); return ptr; } -} \ No newline at end of file +} diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index e3e646ff2a..83850e780c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -16,8 +16,6 @@ class WindowBaseImpl : public virtual ComObject, public virtual IAvnWindowBase, public INSWindowHolder { -private: - NSCursor *cursor; public: FORWARD_IUNKNOWN() @@ -28,23 +26,6 @@ BEGIN_INTERFACE_MAP() virtual ~WindowBaseImpl(); - AutoFitContentView *StandardContainer; - AvnView *View; - NSWindow * Window; - ComPtr BaseEvents; - ComPtr _glContext; - NSObject *renderTarget; - AvnPoint lastPositionSet; - bool hasPosition; - NSSize lastSize; - NSSize lastMinSize; - NSSize lastMaxSize; - AvnMenu* lastMenu; - NSString *_lastTitle; - - bool _shown; - bool _inResize; - WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl); virtual HRESULT ObtainNSWindowHandle(void **ret) override; @@ -123,11 +104,33 @@ protected: virtual NSWindowStyleMask GetStyle(); void UpdateStyle(); + + virtual void OnInitialiseNSWindow (); private: void CreateNSWindow (bool isDialog); void CleanNSWindow (); void InitialiseNSWindow (); + + NSCursor *cursor; + ComPtr _glContext; + bool hasPosition; + NSSize lastSize; + NSSize lastMinSize; + NSSize lastMaxSize; + AvnMenu* lastMenu; + bool _inResize; + +protected: + AvnPoint lastPositionSet; + AutoFitContentView *StandardContainer; + bool _shown; + +public: + NSObject *renderTarget; + NSWindow * Window; + ComPtr BaseEvents; + AvnView *View; }; #endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index bc512f80d8..022769bad0 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -35,7 +35,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) lastSize = NSSize { 100, 100 }; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - _lastTitle = @""; Window = nullptr; lastMenu = nullptr; @@ -102,8 +101,6 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { UpdateStyle(); - [Window setTitle:_lastTitle]; - if (ShouldTakeFocusOnShow() && activate) { [Window orderFront:Window]; [Window makeKeyAndOrderFront:Window]; @@ -570,6 +567,11 @@ void WindowBaseImpl::CreateNSWindow(bool isDialog) { } } +void WindowBaseImpl::OnInitialiseNSWindow() +{ + +} + void WindowBaseImpl::InitialiseNSWindow() { if(Window != nullptr) { [Window setContentView:StandardContainer]; @@ -589,6 +591,8 @@ void WindowBaseImpl::InitialiseNSWindow() { [GetWindowProtocol() showWindowMenuWithAppMenu]; } } + + OnInitialiseNSWindow(); } } diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index a4ee4f447c..db19497b29 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -88,9 +88,14 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetWindowState (AvnWindowState state) override; virtual bool IsDialog() override; + + virtual void OnInitialiseNSWindow() override; protected: virtual NSWindowStyleMask GetStyle() override; + +private: + NSString *_lastTitle; }; #endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 5b15b4cdfc..d96fe717ab 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -19,10 +19,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase _inSetWindowState = false; _lastWindowState = Normal; _actualWindowState = Normal; + _lastTitle = @""; WindowEvents = events; - [Window disableCursorRects]; - [Window setTabbingMode:NSWindowTabbingModeDisallowed]; - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; } void WindowImpl::HideOrShowTrafficLights() { @@ -50,25 +48,29 @@ void WindowImpl::HideOrShowTrafficLights() { } } +void WindowImpl::OnInitialiseNSWindow(){ + [GetWindowProtocol() setCanBecomeKeyWindow:true]; + [Window disableCursorRects]; + [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + + [Window setTitle:_lastTitle]; + + if(_isClientAreaExtended) + { + [GetWindowProtocol() setIsExtended:true]; + SetExtendClientArea(true); + } +} + HRESULT WindowImpl::Show(bool activate, bool isDialog) { START_COM_CALL; @autoreleasepool { _isDialog = isDialog; - bool created = Window == nullptr; - WindowBaseImpl::Show(activate, isDialog); - if(created) - { - if(_isClientAreaExtended) - { - [GetWindowProtocol() setIsExtended:true]; - SetExtendClientArea(true); - } - } - HideOrShowTrafficLights(); return SetWindowState(_lastWindowState); @@ -521,7 +523,7 @@ bool WindowImpl::IsDialog() { } NSWindowStyleMask WindowImpl::GetStyle() { - unsigned long s = this->_isDialog ? NSWindowStyleMaskUtilityWindow : NSWindowStyleMaskBorderless; + unsigned long s = this->_isDialog ? NSWindowStyleMaskDocModalWindow : NSWindowStyleMaskBorderless; switch (_decorations) { case SystemDecorationsNone: diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index 92194706de..0e5c5869e7 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -22,4 +22,6 @@ -(void) setIsExtended:(bool)value; -(void) disconnectParent; -(bool) isDialog; -@end \ No newline at end of file + +-(void) setCanBecomeKeyWindow:(bool)value; +@end diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index e5f07c90c3..bce924a3f2 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -1,7 +1,8 @@  netstandard2.0 - true + true + enable diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index 4a53c9026f..1c2d768966 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -29,6 +29,7 @@ None Slide Crossfade + 3D Rotation diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs index 66180d4ccb..6b7707be13 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs @@ -45,6 +45,9 @@ namespace ControlCatalog.Pages case 2: _carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25)); break; + case 3: + _carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical); + break; } } } diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 3cadc7243a..fd908a33b6 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -8,7 +8,6 @@ using Avalonia.Dialogs; using Avalonia.Layout; using Avalonia.Markup.Xaml; #pragma warning disable 4014 - namespace ControlCatalog.Pages { public class DialogsPage : UserControl @@ -22,7 +21,7 @@ namespace ControlCatalog.Pages string lastSelectedDirectory = null; - List GetFilters() + List? GetFilters() { if (this.FindControl("UseFilters").IsChecked != true) return null; diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 429c4776d5..e6e62f86ed 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -72,5 +72,8 @@ + + + diff --git a/samples/RenderDemo/Pages/Transform3DPage.axaml b/samples/RenderDemo/Pages/Transform3DPage.axaml new file mode 100644 index 0000000000..30ed35bbac --- /dev/null +++ b/samples/RenderDemo/Pages/Transform3DPage.axaml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/RenderDemo/Pages/Transform3DPage.axaml.cs b/samples/RenderDemo/Pages/Transform3DPage.axaml.cs new file mode 100644 index 0000000000..5083189c4c --- /dev/null +++ b/samples/RenderDemo/Pages/Transform3DPage.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using RenderDemo.ViewModels; + +namespace RenderDemo.Pages; + +public class Transform3DPage : UserControl +{ + public Transform3DPage() + { + InitializeComponent(); + this.DataContext = new Transform3DPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} + diff --git a/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs b/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs new file mode 100644 index 0000000000..c8d1d40e3a --- /dev/null +++ b/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs @@ -0,0 +1,55 @@ +using System; +using MiniMvvm; +using Avalonia.Animation; + +namespace RenderDemo.ViewModels +{ + public class Transform3DPageViewModel : ViewModelBase + { + private double _depth = 200; + + private double _centerX = 0; + private double _centerY = 0; + private double _centerZ = 0; + private double _angleX = 0; + private double _angleY = 0; + private double _angleZ = 0; + + public double Depth + { + get => _depth; + set => RaiseAndSetIfChanged(ref _depth, value); + } + + public double CenterX + { + get => _centerX; + set => RaiseAndSetIfChanged(ref _centerX, value); + } + public double CenterY + { + get => _centerY; + set => RaiseAndSetIfChanged(ref _centerY, value); + } + public double CenterZ + { + get => _centerZ; + set => RaiseAndSetIfChanged(ref _centerZ, value); + } + public double AngleX + { + get => _angleX; + set => RaiseAndSetIfChanged(ref _angleX, value); + } + public double AngleY + { + get => _angleY; + set => RaiseAndSetIfChanged(ref _angleY, value); + } + public double AngleZ + { + get => _angleZ; + set => RaiseAndSetIfChanged(ref _angleZ, value); + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 8a475676a5..65a9adc937 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -40,7 +40,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _gl = GlPlatformSurface.TryCreate(this); _framebuffer = new FramebufferManager(this); - RenderScaling = (int)_view.Scaling; + RenderScaling = _view.Scaling; MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); diff --git a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs index 34ec8ac503..e12ca722f9 100644 --- a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs @@ -43,6 +43,7 @@ namespace Avalonia.Animation.Animators normalTransform.Children.Add(new SkewTransform()); normalTransform.Children.Add(new RotateTransform()); normalTransform.Children.Add(new TranslateTransform()); + normalTransform.Children.Add(new Rotate3DTransform()); ctrl.RenderTransform = normalTransform; } diff --git a/src/Avalonia.Base/Animation/PageSlide.cs b/src/Avalonia.Base/Animation/PageSlide.cs index 6f5d12d824..b22bc8b243 100644 --- a/src/Avalonia.Base/Animation/PageSlide.cs +++ b/src/Avalonia.Base/Animation/PageSlide.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Animation { /// - /// Transitions between two pages by sliding them horizontally. + /// Transitions between two pages by sliding them horizontally or vertically. /// public class PageSlide : IPageTransition { @@ -62,7 +62,7 @@ namespace Avalonia.Animation public Easing SlideOutEasing { get; set; } = new LinearEasing(); /// - public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) + public virtual async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -157,7 +157,7 @@ namespace Avalonia.Animation /// /// Any one of the parameters may be null, but not both. /// - private static IVisual GetVisualParent(IVisual? from, IVisual? to) + protected static IVisual GetVisualParent(IVisual? from, IVisual? to) { var p1 = (from ?? to)!.VisualParent; var p2 = (to ?? from)!.VisualParent; diff --git a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs new file mode 100644 index 0000000000..239f3aea08 --- /dev/null +++ b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Media; +using Avalonia.Styling; + +namespace Avalonia.Animation; + +public class Rotate3DTransition: PageSlide +{ + + /// + /// Creates a new instance of the + /// + /// How long the rotation should take place + /// The orientation of the rotation + /// Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height of the common parent of the visual being rotated + public Rotate3DTransition(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal, double? depth = null) + : base(duration, orientation) + { + Depth = depth; + } + + /// + /// Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height + /// of the common parent of the visual being rotated. + /// + public double? Depth { get; set; } + + /// + /// Creates a new instance of the + /// + public Rotate3DTransition() { } + + /// + public override async Task Start(Visual? @from, Visual? to, bool forward, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var tasks = new Task[from != null && to != null ? 2 : 1]; + var parent = GetVisualParent(from, to); + var (rotateProperty, center) = Orientation switch + { + SlideAxis.Vertical => (Rotate3DTransform.AngleXProperty, parent.Bounds.Height), + SlideAxis.Horizontal => (Rotate3DTransform.AngleYProperty, parent.Bounds.Width), + _ => throw new ArgumentOutOfRangeException() + }; + + var depthSetter = new Setter {Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center}; + var centerZSetter = new Setter {Property = Rotate3DTransform.CenterZProperty, Value = -center / 2}; + + KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) => + new() { + Setters = + { + new Setter { Property = rotateProperty, Value = rotation }, + new Setter { Property = Visual.ZIndexProperty, Value = zIndex }, + new Setter { Property = Visual.IsVisibleProperty, Value = isVisible }, + centerZSetter, + depthSetter + }, + Cue = new Cue(cue) + }; + + if (from != null) + { + var animation = new Animation + { + Easing = SlideOutEasing, + Duration = Duration, + FillMode = FillMode.Forward, + Children = + { + CreateKeyFrame(0d, 0d, 2), + CreateKeyFrame(0.5d, 45d * (forward ? -1 : 1), 1), + CreateKeyFrame(1d, 90d * (forward ? -1 : 1), 1, isVisible: false) + } + }; + + tasks[0] = animation.RunAsync(from, null, cancellationToken); + } + + if (to != null) + { + to.IsVisible = true; + var animation = new Animation + { + Easing = SlideInEasing, + Duration = Duration, + FillMode = FillMode.Forward, + Children = + { + CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1), + CreateKeyFrame(0.5d, 45d * (forward ? 1 : -1), 1), + CreateKeyFrame(1d, 0d, 2) + } + }; + + tasks[from != null ? 1 : 0] = animation.RunAsync(to, null, cancellationToken); + } + + await Task.WhenAll(tasks); + + if (!cancellationToken.IsCancellationRequested) + { + if (to != null) + { + to.ZIndex = 2; + } + + if (from != null) + { + from.IsVisible = false; + from.ZIndex = 1; + } + } + } +} diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index 445f35aad2..45c67b9f48 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -55,7 +55,7 @@ namespace Avalonia /// /// /// This will usually be true, except in - /// + /// /// which receives notifications for all changes to property values, whether a value with a higher /// priority is present or not. When this property is false, the change that is being signaled /// has not resulted in a change to the property value on the object. diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index b08a0eb98a..5bbc657385 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -1,12 +1,22 @@ using System; using System.Globalization; +using System.Linq; +using System.Numerics; using Avalonia.Utilities; namespace Avalonia { /// - /// A 2x3 matrix. + /// A 3x3 matrix. /// + /// Matrix layout: + /// | 1st col | 2nd col | 3r col | + /// 1st row | scaleX | skrewY | persX | + /// 2nd row | skrewX | scaleY | persY | + /// 3rd row | transX | transY | persZ | + /// + /// Note: Skia.SkMatrix uses a transposed layout (where for example skrewX/skrewY and perspp0/tranX are swapped). + /// #if !BUILDTASK public #endif @@ -14,40 +24,76 @@ namespace Avalonia { private readonly double _m11; private readonly double _m12; + private readonly double _m13; private readonly double _m21; private readonly double _m22; + private readonly double _m23; private readonly double _m31; private readonly double _m32; + private readonly double _m33; + + /// + /// Initializes a new instance of the struct (equivalent to a 2x3 Matrix without perspective). + /// + /// The first element of the first row. + /// The second element of the first row. + /// The first element of the second row. + /// The second element of the second row. + /// The first element of the third row. + /// The second element of the third row. + public Matrix( + double scaleX, + double skrewY, + double skrewX, + double scaleY, + double offsetX, + double offsetY) : this( scaleX, skrewY, 0, skrewX, scaleY, 0, offsetX, offsetY, 1) + { + } + + /// /// Initializes a new instance of the struct. /// - /// The first element of the first row. - /// The second element of the first row. - /// The first element of the second row. - /// The second element of the second row. + /// The first element of the first row. + /// The second element of the first row. + /// The third element of the first row. + /// The first element of the second row. + /// The second element of the second row. + /// The third element of the second row. /// The first element of the third row. /// The second element of the third row. + /// The third element of the third row. public Matrix( - double m11, - double m12, - double m21, - double m22, + double scaleX, + double skrewY, + double persX, + double skrewX, + double scaleY, + double persY, double offsetX, - double offsetY) + double offsetY, + double persZ) { - _m11 = m11; - _m12 = m12; - _m21 = m21; - _m22 = m22; + _m11 = scaleX; + _m12 = skrewY; + _m13 = persX; + _m21 = skrewX; + _m22 = scaleY; + _m23 = persY; _m31 = offsetX; _m32 = offsetY; + _m33 = persZ; } /// /// Returns the multiplicative identity matrix. /// - public static Matrix Identity { get; } = new Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0); + public static Matrix Identity { get; } = new Matrix( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0); /// /// Returns whether the matrix is the identity matrix. @@ -60,35 +106,50 @@ namespace Avalonia public bool HasInverse => !MathUtilities.IsZero(GetDeterminant()); /// - /// The first element of the first row + /// The first element of the first row (scaleX). /// public double M11 => _m11; /// - /// The second element of the first row + /// The second element of the first row (skrewY). /// public double M12 => _m12; /// - /// The first element of the second row + /// The third element of the first row (persX: input x-axis perspective factor). + /// + public double M13 => _m13; + + /// + /// The first element of the second row (skrewX). /// public double M21 => _m21; /// - /// The second element of the second row + /// The second element of the second row (scaleY). /// public double M22 => _m22; /// - /// The first element of the third row + /// The third element of the second row (persY: input y-axis perspective factor). + /// + public double M23 => _m23; + + /// + /// The first element of the third row (offsetX/translateX). /// public double M31 => _m31; /// - /// The second element of the third row + /// The second element of the third row (offsetY/translateY). /// public double M32 => _m32; + /// + /// The third element of the third row (persZ: perspective scale factor). + /// + public double M33 => _m33; + /// /// Multiplies two matrices together and returns the resulting matrix. /// @@ -98,12 +159,15 @@ namespace Avalonia public static Matrix operator *(Matrix value1, Matrix value2) { return new Matrix( - (value1.M11 * value2.M11) + (value1.M12 * value2.M21), - (value1.M11 * value2.M12) + (value1.M12 * value2.M22), - (value1.M21 * value2.M11) + (value1.M22 * value2.M21), - (value1.M21 * value2.M12) + (value1.M22 * value2.M22), - (value1._m31 * value2.M11) + (value1._m32 * value2.M21) + value2._m31, - (value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32); + (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31), + (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32), + (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33), + (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31), + (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32), + (value1.M21 * value2.M13) + (value1.M22 * value2.M23) + (value1.M23 * value2.M33), + (value1.M31 * value2.M11) + (value1.M32 * value2.M21) + (value1.M33 * value2.M31), + (value1.M31 * value2.M12) + (value1.M32 * value2.M22) + (value1.M33 * value2.M32), + (value1.M31 * value2.M13) + (value1.M32 * value2.M23) + (value1.M33 * value2.M33)); } /// @@ -171,7 +235,7 @@ namespace Avalonia /// A scaling matrix. public static Matrix CreateScale(double xScale, double yScale) { - return CreateScale(new Vector(xScale, yScale)); + return new Matrix(xScale, 0, 0, yScale, 0, 0); } /// @@ -181,7 +245,7 @@ namespace Avalonia /// A scaling matrix. public static Matrix CreateScale(Vector scales) { - return new Matrix(scales.X, 0, 0, scales.Y, 0, 0); + return CreateScale(scales.X, scales.Y); } /// @@ -214,7 +278,7 @@ namespace Avalonia { return angle * 0.0174532925; } - + /// /// Appends another matrix as post-multiplication operation. /// Equivalent to this * value; @@ -227,7 +291,7 @@ namespace Avalonia } /// - /// Prpends another matrix as pre-multiplication operation. + /// Prepends another matrix as pre-multiplication operation. /// Equivalent to value * this; /// /// A matrix. @@ -247,7 +311,49 @@ namespace Avalonia /// public double GetDeterminant() { - return (_m11 * _m22) - (_m12 * _m21); + // implemented using "Laplace expansion": + return _m11 * (_m22 * _m33 - _m23 * _m32) + - _m12 * (_m21 * _m33 - _m23 * _m31) + + _m13 * (_m21 * _m32 - _m22 * _m31); + } + + /// + /// Transforms the point with the matrix + /// + /// The point to be transformed + /// The transformed point + public Point Transform(Point p) + { + Point transformedResult; + + // If this matrix contains a non-affine transform with need to extend + // the point to a 3D vector and flatten it back for 2d display + // by multiplying X and Y with the inverse of the Z axis. + // The code below also works with affine transformations, but for performance (and compatibility) + // reasons we will use the more complex calculation only if necessary + if (ContainsPerspective()) + { + var m44 = new Matrix4x4( + (float)M11, (float)M12, (float)M13, 0, + (float)M21, (float)M22, (float)M23, 0, + (float)M31, (float)M32, (float)M33, 0, + 0, 0, 0, 1 + ); + + var vector = new Vector3((float)p.X, (float)p.Y, 1); + var transformedVector = Vector3.Transform(vector, m44); + var z = 1 / transformedVector.Z; + + transformedResult = new Point(transformedVector.X * z, transformedVector.Y * z); + } + else + { + return new Point( + (p.X * M11) + (p.Y * M21) + M31, + (p.X * M12) + (p.Y * M22) + M32); + } + + return transformedResult; } /// @@ -260,10 +366,13 @@ namespace Avalonia // ReSharper disable CompareOfFloatsByEqualityOperator return _m11 == other.M11 && _m12 == other.M12 && + _m13 == other.M13 && _m21 == other.M21 && _m22 == other.M22 && + _m23 == other.M23 && _m31 == other.M31 && - _m32 == other.M32; + _m32 == other.M32 && + _m33 == other.M33; // ReSharper restore CompareOfFloatsByEqualityOperator } @@ -280,9 +389,18 @@ namespace Avalonia /// The hash code. public override int GetHashCode() { - return M11.GetHashCode() + M12.GetHashCode() + - M21.GetHashCode() + M22.GetHashCode() + - M31.GetHashCode() + M32.GetHashCode(); + return (_m11, _m12, _m13, _m21, _m22, _m23, _m31, _m32, _m33).GetHashCode(); + } + + /// + /// Determines if the current matrix contains perspective (non-affine) transforms (true) or only (affine) transforms that could be mapped into an 2x3 matrix (false). + /// + public bool ContainsPerspective() + { + + // ReSharper disable CompareOfFloatsByEqualityOperator + return _m13 != 0 || _m23 != 0 || _m33 != 1; + // ReSharper restore CompareOfFloatsByEqualityOperator } /// @@ -292,15 +410,25 @@ namespace Avalonia public override string ToString() { CultureInfo ci = CultureInfo.CurrentCulture; + + string msg; + double[] values; + + if (ContainsPerspective()) + { + msg = "{{ {{M11:{0} M12:{1} M13:{2}}} {{M21:{3} M22:{4} M23:{5}}} {{M31:{6} M32:{7} M33:{8}}} }}"; + values = new[] { M11, M12, M13, M21, M22, M23, M31, M32, M33 }; + } + else + { + msg = "{{ {{M11:{0} M12:{1}}} {{M21:{2} M22:{3}}} {{M31:{4} M32:{5}}} }}"; + values = new[] { M11, M12, M21, M22, M31, M32 }; + } + return string.Format( ci, - "{{ {{M11:{0} M12:{1}}} {{M21:{2} M22:{3}}} {{M31:{4} M32:{5}}} }}", - M11.ToString(ci), - M12.ToString(ci), - M21.ToString(ci), - M22.ToString(ci), - M31.ToString(ci), - M32.ToString(ci)); + msg, + values.Select((v) => v.ToString(ci)).ToArray()); } /// @@ -318,14 +446,20 @@ namespace Avalonia return false; } + var invdet = 1 / d; + inverted = new Matrix( - _m22 / d, - -_m12 / d, - -_m21 / d, - _m11 / d, - ((_m21 * _m32) - (_m22 * _m31)) / d, - ((_m12 * _m31) - (_m11 * _m32)) / d); - + (_m22 * _m33 - _m32 * _m23) * invdet, + (_m13 * _m31 - _m12 * _m33) * invdet, + (_m12 * _m23 - _m13 * _m22) * invdet, + (_m23 * _m31 - _m21 * _m33) * invdet, + (_m11 * _m33 - _m13 * _m31) * invdet, + (_m21 * _m13 - _m11 * _m23) * invdet, + (_m21 * _m32 - _m31 * _m22) * invdet, + (_m21 * _m12 - _m11 * _m32) * invdet, + (_m11 * _m22 - _m21 * _m12) * invdet + ); + return true; } @@ -336,7 +470,7 @@ namespace Avalonia /// The inverted matrix. public Matrix Invert() { - if (!TryInvert(out Matrix inverted)) + if (!TryInvert(out var inverted)) { throw new InvalidOperationException("Transform is not invertible."); } @@ -347,20 +481,30 @@ namespace Avalonia /// /// Parses a string. /// - /// Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new + /// Six or nine comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY[, persX, persY, persZ]) that describe the new /// The . public static Matrix Parse(string s) { + // initialize to satisfy compiler - only used when retrieved from string. + double v8 = 0; + double v9 = 0; + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix.")) { - return new Matrix( - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble(), - tokenizer.ReadDouble() - ); + var v1 = tokenizer.ReadDouble(); + var v2 = tokenizer.ReadDouble(); + var v3 = tokenizer.ReadDouble(); + var v4 = tokenizer.ReadDouble(); + var v5 = tokenizer.ReadDouble(); + var v6 = tokenizer.ReadDouble(); + var pers = tokenizer.TryReadDouble(out var v7); + pers = pers && tokenizer.TryReadDouble(out v8); + pers = pers && tokenizer.TryReadDouble(out v9); + + if (pers) + return new Matrix(v1, v2, v7, v3, v4, v8, v5, v6, v9); + else + return new Matrix(v1, v2, v3, v4, v5, v6); } } @@ -369,14 +513,14 @@ namespace Avalonia /// /// Matrix to decompose. /// Decomposed matrix. - /// The status of the operation. + /// The status of the operation. public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) { decomposed = default; var determinant = matrix.GetDeterminant(); - if (MathUtilities.IsZero(determinant)) + if (MathUtilities.IsZero(determinant) || matrix.ContainsPerspective()) { return false; } diff --git a/src/Avalonia.Base/Point.cs b/src/Avalonia.Base/Point.cs index 67e7d71fbc..fbdf0db800 100644 --- a/src/Avalonia.Base/Point.cs +++ b/src/Avalonia.Base/Point.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; #if !BUILDTASK using Avalonia.Animation.Animators; #endif @@ -168,12 +169,7 @@ namespace Avalonia /// The point. /// The matrix. /// The resulting point. - public static Point operator *(Point point, Matrix matrix) - { - return new Point( - (point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31, - (point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32); - } + public static Point operator *(Point point, Matrix matrix) => matrix.Transform(point); /// /// Parses a string. @@ -242,18 +238,7 @@ namespace Avalonia /// /// The transform. /// The transformed point. - public Point Transform(Matrix transform) - { - var x = X; - var y = Y; - var xadd = y * transform.M21 + transform.M31; - var yadd = x * transform.M12 + transform.M32; - x *= transform.M11; - x += xadd; - y *= transform.M22; - y += yadd; - return new Point(x, y); - } + public Point Transform(Matrix transform) => transform.Transform(this); /// /// Returns a new point with the specified X coordinate. diff --git a/src/Avalonia.Base/Rotate3DTransform.cs b/src/Avalonia.Base/Rotate3DTransform.cs new file mode 100644 index 0000000000..2c4e515861 --- /dev/null +++ b/src/Avalonia.Base/Rotate3DTransform.cs @@ -0,0 +1,210 @@ +using System; +using System.Numerics; +using Avalonia.Animation.Animators; + +namespace Avalonia.Media; + +/// +/// Non-Affine 3D transformation for rotating a visual around a definable axis +/// +public class Rotate3DTransform : Transform +{ + private readonly bool _isInitializing; + + /// + /// Defines the property. + /// + public static readonly StyledProperty AngleXProperty = + AvaloniaProperty.Register(nameof(AngleX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AngleYProperty = + AvaloniaProperty.Register(nameof(AngleY)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AngleZProperty = + AvaloniaProperty.Register(nameof(AngleZ)); + + + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterXProperty = + AvaloniaProperty.Register(nameof(CenterX)); + + + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterYProperty = + AvaloniaProperty.Register(nameof(CenterY)); + + + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterZProperty = + AvaloniaProperty.Register(nameof(CenterZ)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DepthProperty = + AvaloniaProperty.Register(nameof(Depth)); + + /// + /// Initializes a new instance of the class. + /// + public Rotate3DTransform() { } + + /// + /// Initializes a new instance of the class. + /// + /// The rotation around the X-Axis + /// The rotation around the Y-Axis + /// The rotation around the Z-Axis + /// The origin of the X-Axis + /// The origin of the Y-Axis + /// The origin of the Z-Axis + /// The depth of the 3D effect + public Rotate3DTransform( + double angleX, + double angleY, + double angleZ, + double centerX, + double centerY, + double centerZ, + double depth) : this() + { + _isInitializing = true; + AngleX = angleX; + AngleY = angleY; + AngleZ = angleZ; + CenterX = centerX; + CenterY = centerY; + CenterZ = centerZ; + Depth = depth; + _isInitializing = false; + } + + /// + /// Sets the rotation around the X-Axis + /// + public double AngleX + { + get => GetValue(AngleXProperty); + set => SetValue(AngleXProperty, value); + } + + /// + /// Sets the rotation around the Y-Axis + /// + public double AngleY + { + get => GetValue(AngleYProperty); + set => SetValue(AngleYProperty, value); + } + + /// + /// Sets the rotation around the Z-Axis + /// + public double AngleZ + { + get => GetValue(AngleZProperty); + set => SetValue(AngleZProperty, value); + } + + /// + /// Moves the origin the X-Axis rotates around + /// + public double CenterX + { + get => GetValue(CenterXProperty); + set => SetValue(CenterXProperty, value); + } + + /// + /// Moves the origin the Y-Axis rotates around + /// + public double CenterY + { + get => GetValue(CenterYProperty); + set => SetValue(CenterYProperty, value); + } + + /// + /// Moves the origin the Z-Axis rotates around + /// + public double CenterZ + { + get => GetValue(CenterZProperty); + set => SetValue(CenterZProperty, value); + } + + /// + /// Affects the depth of the rotation effect + /// + public double Depth + { + get => GetValue(DepthProperty); + set => SetValue(DepthProperty, value); + } + + /// + /// Gets the transform's . + /// + public override Matrix Value + { + get + { + var matrix44 = Matrix4x4.Identity; + //Copy values first, because it's not guaranteed, that values will not change during calculation + var (copyCenterX, + copyCenterY, + copyCenterZ, + copyAngleX, + copyAngleY, + copyAngleZ, + copyDepth) = (CenterX, CenterY, CenterZ, AngleX, AngleY, AngleZ, Depth); + + var centerSum = copyCenterX + copyCenterY + copyCenterZ; + + if (Math.Abs(centerSum) > double.Epsilon) matrix44 *= Matrix4x4.CreateTranslation(-(float)copyCenterX, -(float)copyCenterY, -(float)copyCenterZ); + + if (copyAngleX != 0) matrix44 *= Matrix4x4.CreateRotationX((float)Matrix.ToRadians(copyAngleX)); + if (copyAngleY != 0) matrix44 *= Matrix4x4.CreateRotationY((float)Matrix.ToRadians(copyAngleY)); + if (copyAngleZ != 0) matrix44 *= Matrix4x4.CreateRotationZ((float)Matrix.ToRadians(copyAngleZ)); + + if (Math.Abs(centerSum) > double.Epsilon) matrix44 *= Matrix4x4.CreateTranslation((float)copyCenterX, (float)copyCenterY, (float)copyCenterZ); + + if (copyDepth != 0) + { + var perspectiveMatrix = Matrix4x4.Identity; + perspectiveMatrix.M34 = -1 / (float)copyDepth; + matrix44 *= perspectiveMatrix; + } + + var matrix = new Matrix( + matrix44.M11, + matrix44.M12, + matrix44.M14, + matrix44.M21, + matrix44.M22, + matrix44.M24, + matrix44.M41, + matrix44.M42, + matrix44.M44); + + return matrix; + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (!_isInitializing) RaiseChanged(); + } +} diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 5c92182b80..34f3a76b61 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -37,13 +37,13 @@ namespace Avalonia.Styling return _selectorString; } - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { var controlParent = ((ILogical)control).LogicalParent; if (controlParent != null) { - var parentMatch = _parent.Match((IStyleable)controlParent, subscribe); + var parentMatch = _parent.Match((IStyleable)controlParent, parent, subscribe); if (parentMatch.Result == SelectorMatchResult.Sometimes) { @@ -65,5 +65,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; + internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); } } diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index dde88b3436..4ffaff6861 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -35,7 +35,7 @@ namespace Avalonia.Styling return _selectorString; } - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { var c = (ILogical)control; var descendantMatches = new OrActivatorBuilder(); @@ -46,7 +46,7 @@ namespace Avalonia.Styling if (c is IStyleable) { - var match = _parent.Match((IStyleable)c, subscribe); + var match = _parent.Match((IStyleable)c, parent, subscribe); if (match.Result == SelectorMatchResult.Sometimes) { @@ -70,5 +70,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => null; + internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector(); } } diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs new file mode 100644 index 0000000000..481a937867 --- /dev/null +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -0,0 +1,30 @@ +using System; + +namespace Avalonia.Styling +{ + /// + /// The `^` nesting style selector. + /// + internal class NestingSelector : Selector + { + public override bool InTemplate => false; + public override bool IsCombinator => false; + public override Type? TargetType => null; + + public override string ToString() => "^"; + + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) + { + if (parent is Style s && s.Selector is Selector selector) + { + return selector.Match(control, (parent as Style)?.Parent, subscribe); + } + + throw new InvalidOperationException( + "Nesting selector was specified but cannot determine parent selector."); + } + + protected override Selector? MovePrevious() => null; + internal override bool HasValidNestingSelector() => true; + } +} diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs index ab4e9d5d7f..cdc3254d38 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -45,9 +45,9 @@ namespace Avalonia.Styling return _selectorString; } - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { - var innerResult = _argument.Match(control, subscribe); + var innerResult = _argument.Match(control, parent, subscribe); switch (innerResult.Result) { @@ -67,5 +67,6 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; + internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector(); } } diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index aff34ea17c..047bf434da 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -48,7 +48,7 @@ namespace Avalonia.Styling public int Step { get; } public int Offset { get; } - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { if (!(control is ILogical logical)) { @@ -105,6 +105,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; + internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; public override string ToString() { diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index 3d6db9b01e..913c27bf0c 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -65,14 +65,14 @@ namespace Avalonia.Styling return _selectorString; } - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { var activators = new OrActivatorBuilder(); var neverThisInstance = false; foreach (var selector in _selectors) { - var match = selector.Match(control, subscribe); + var match = selector.Match(control, parent, subscribe); switch (match.Result) { @@ -104,6 +104,19 @@ namespace Avalonia.Styling protected override Selector? MovePrevious() => null; + internal override bool HasValidNestingSelector() + { + foreach (var selector in _selectors) + { + if (!selector.HasValidNestingSelector()) + { + return false; + } + } + + return true; + } + private Type? EvaluateTargetType() { Type? result = null; diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 1cd1a650ef..7a37daf087 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -74,7 +74,7 @@ namespace Avalonia.Styling } /// - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { if (subscribe) { @@ -90,6 +90,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; + internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; internal static bool Compare(Type propertyType, object? propertyValue, object? value) { diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 0740e0f891..1e06f3d375 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -33,22 +33,25 @@ namespace Avalonia.Styling /// Tries to match the selector with a control. /// /// The control. + /// + /// The parent style, if the style containing the selector is a nested style. + /// /// /// Whether the match should subscribe to changes in order to track the match over time, /// or simply return an immediate result. /// /// A . - public SelectorMatch Match(IStyleable control, bool subscribe = true) + public SelectorMatch Match(IStyleable control, IStyle? parent = null, bool subscribe = true) { // First match the selector until a combinator is found. Selectors are stored from // right-to-left, so MatchUntilCombinator reverses this order because the type selector // will be on the left. - var match = MatchUntilCombinator(control, this, subscribe, out var combinator); + var match = MatchUntilCombinator(control, this, parent, subscribe, out var combinator); // If the pre-combinator selector matches, we can now match the combinator, if any. if (match.IsMatch && combinator is object) { - match = match.And(combinator.Match(control, subscribe)); + match = match.And(combinator.Match(control, parent, subscribe)); // If we have a combinator then we can never say that we always match a control of // this type, because by definition the combinator matches on things outside of the @@ -68,28 +71,34 @@ namespace Avalonia.Styling /// Evaluates the selector for a match. /// /// The control. + /// + /// The parent style, if the style containing the selector is a nested style. + /// /// /// Whether the match should subscribe to changes in order to track the match over time, /// or simply return an immediate result. /// /// A . - protected abstract SelectorMatch Evaluate(IStyleable control, bool subscribe); + protected abstract SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe); /// /// Moves to the previous selector. /// protected abstract Selector? MovePrevious(); + internal abstract bool HasValidNestingSelector(); + private static SelectorMatch MatchUntilCombinator( IStyleable control, Selector start, + IStyle? parent, bool subscribe, out Selector? combinator) { combinator = null; var activators = new AndActivatorBuilder(); - var result = Match(control, start, subscribe, ref activators, ref combinator); + var result = Match(control, start, parent, subscribe, ref activators, ref combinator); return result == SelectorMatchResult.Sometimes ? new SelectorMatch(activators.Get()) : @@ -99,6 +108,7 @@ namespace Avalonia.Styling private static SelectorMatchResult Match( IStyleable control, Selector selector, + IStyle? parent, bool subscribe, ref AndActivatorBuilder activators, ref Selector? combinator) @@ -110,7 +120,7 @@ namespace Avalonia.Styling // opportunity to exit early. if (previous != null && !previous.IsCombinator) { - var previousMatch = Match(control, previous, subscribe, ref activators, ref combinator); + var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); if (previousMatch < SelectorMatchResult.Sometimes) { @@ -119,7 +129,7 @@ namespace Avalonia.Styling } // Match this selector. - var match = selector.Evaluate(control, subscribe); + var match = selector.Evaluate(control, parent, subscribe); if (!match.IsMatch) { diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs index 7c66469cf1..476d86cd11 100644 --- a/src/Avalonia.Base/Styling/Selectors.cs +++ b/src/Avalonia.Base/Styling/Selectors.cs @@ -109,6 +109,11 @@ namespace Avalonia.Styling } } + public static Selector Nesting(this Selector? previous) + { + return new NestingSelector(); + } + /// /// Returns a selector which inverts the results of selector argument. /// diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 00819ef7be..8fcf5eec8a 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -4,8 +4,6 @@ using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; -#nullable enable - namespace Avalonia.Styling { /// @@ -14,9 +12,11 @@ namespace Avalonia.Styling public class Style : AvaloniaObject, IStyle, IResourceProvider { private IResourceHost? _owner; + private StyleChildren? _children; private IResourceDictionary? _resources; private List? _setters; private List? _animations; + private StyleCache? _childCache; /// /// Initializes a new instance of the class. @@ -34,6 +34,14 @@ namespace Avalonia.Styling Selector = selector(null); } + /// + /// Gets the children of the style. + /// + public IList Children => _children ??= new(this); + + /// + /// Gets the or Application that hosts the style. + /// public IResourceHost? Owner { get => _owner; @@ -47,6 +55,11 @@ namespace Avalonia.Styling } } + /// + /// Gets the parent style if this style is hosted in a collection. + /// + public Style? Parent { get; private set; } + /// /// Gets or sets a dictionary of style resources. /// @@ -90,7 +103,7 @@ namespace Avalonia.Styling public IList Animations => _animations ??= new List(); bool IResourceNode.HasResources => _resources?.Count > 0; - IReadOnlyList IStyle.Children => Array.Empty(); + IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty(); public event EventHandler? OwnerChanged; @@ -98,7 +111,7 @@ namespace Avalonia.Styling { target = target ?? throw new ArgumentNullException(nameof(target)); - var match = Selector is object ? Selector.Match(target) : + var match = Selector is object ? Selector.Match(target, Parent) : target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; if (match.IsMatch && (_setters is object || _animations is object)) @@ -108,7 +121,17 @@ namespace Avalonia.Styling instance.Start(); } - return match.Result; + var result = match.Result; + + if (_children is not null) + { + _childCache ??= new StyleCache(); + var childResult = _childCache.TryAttach(_children, target, host); + if (childResult > result) + result = childResult; + } + + return result; } public bool TryGetResource(object key, out object? result) @@ -156,5 +179,18 @@ namespace Avalonia.Styling _resources?.RemoveOwner(owner); } } + + internal void SetParent(Style? parent) + { + if (parent?.Selector is not null) + { + if (Selector is null) + throw new InvalidOperationException("Child styles must have a selector."); + if (!Selector.HasValidNestingSelector()) + throw new InvalidOperationException("Child styles must have a nesting selector."); + } + + Parent = parent; + } } } diff --git a/src/Avalonia.Base/Styling/StyleCache.cs b/src/Avalonia.Base/Styling/StyleCache.cs new file mode 100644 index 0000000000..3285476880 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleCache.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Styling +{ + /// + /// Simple cache for improving performance of applying styles. + /// + /// + /// Maps to a list of styles that are known be be possible + /// matches. + /// + internal class StyleCache : Dictionary?> + { + public SelectorMatchResult TryAttach(IList styles, IStyleable target, IStyleHost? host) + { + if (TryGetValue(target.StyleKey, out var cached)) + { + if (cached is object) + { + var result = SelectorMatchResult.NeverThisType; + + foreach (var style in cached) + { + var childResult = style.TryAttach(target, host); + if (childResult > result) + result = childResult; + } + + return result; + } + else + { + return SelectorMatchResult.NeverThisType; + } + } + else + { + List? matches = null; + + foreach (var child in styles) + { + if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) + { + matches ??= new List(); + matches.Add(child); + } + } + + Add(target.StyleKey, matches); + + return matches is null ? + SelectorMatchResult.NeverThisType : + SelectorMatchResult.AlwaysThisType; + } + } + } +} diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs new file mode 100644 index 0000000000..5f8635f155 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleChildren.cs @@ -0,0 +1,35 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace Avalonia.Styling +{ + internal class StyleChildren : Collection + { + private readonly Style _owner; + + public StyleChildren(Style owner) => _owner = owner; + + protected override void InsertItem(int index, IStyle item) + { + (item as Style)?.SetParent(_owner); + base.InsertItem(index, item); + } + + protected override void RemoveItem(int index) + { + var item = Items[index]; + (item as Style)?.SetParent(null); + if (_owner.Owner is IResourceHost host) + (item as IResourceProvider)?.RemoveOwner(host); + base.RemoveItem(index); + } + + protected override void SetItem(int index, IStyle item) + { + (item as Style)?.SetParent(_owner); + base.SetItem(index, item); + if (_owner.Owner is IResourceHost host) + (item as IResourceProvider)?.AddOwner(host); + } + } +} diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index d79081152e..7c0bc4ad7f 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -20,7 +20,7 @@ namespace Avalonia.Styling private readonly AvaloniaList _styles = new AvaloniaList(); private IResourceHost? _owner; private IResourceDictionary? _resources; - private Dictionary?>? _cache; + private StyleCache? _cache; public Styles() { @@ -111,43 +111,8 @@ namespace Avalonia.Styling public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - _cache ??= new Dictionary?>(); - - if (_cache.TryGetValue(target.StyleKey, out var cached)) - { - if (cached is object) - { - foreach (var style in cached) - { - style.TryAttach(target, host); - } - - return SelectorMatchResult.AlwaysThisType; - } - else - { - return SelectorMatchResult.NeverThisType; - } - } - else - { - List? matches = null; - - foreach (var child in this) - { - if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) - { - matches ??= new List(); - matches.Add(child); - } - } - - _cache.Add(target.StyleKey, matches); - - return matches is null ? - SelectorMatchResult.NeverThisType : - SelectorMatchResult.AlwaysThisType; - } + _cache ??= new StyleCache(); + return _cache.TryAttach(this, target, host); } /// diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index e8051efa6d..b0a2dae8d6 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -36,7 +36,7 @@ namespace Avalonia.Styling return _selectorString; } - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { var templatedParent = control.TemplatedParent as IStyleable; @@ -45,9 +45,10 @@ namespace Avalonia.Styling return SelectorMatch.NeverThisInstance; } - return _parent.Match(templatedParent, subscribe); + return _parent.Match(templatedParent, parent, subscribe); } protected override Selector? MovePrevious() => null; + internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false; } } diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index ef48c4a8cd..24d5d6bbbf 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -94,7 +94,7 @@ namespace Avalonia.Styling } /// - protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { if (TargetType != null) { @@ -140,6 +140,7 @@ namespace Avalonia.Styling } protected override Selector? MovePrevious() => _previous; + internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false; private string BuildSelectorString() { diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e9b99c9aa8..a801d338c3 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -103,5 +103,6 @@ + diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index a3095ad214..ae52e5f970 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -193,8 +193,11 @@ namespace Avalonia.Controls } } + /// Try get number of DataSource itmes. /// When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead. /// If "getAny" is true, method can use Linq.Any() method to speedup. + /// number of DataSource itmes. + /// true if able to retrieve number of DataSource itmes; otherwise, false. internal bool TryGetCount(bool allowSlow, bool getAny, out int count) { bool result; diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 3b38750de7..3a6d06f150 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -56,6 +56,7 @@ namespace Avalonia.Controls.Platform Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved); _root = Menu.VisualRoot; @@ -91,6 +92,7 @@ namespace Avalonia.Controls.Platform Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved); if (_root is InputElement inputRoot) { @@ -340,6 +342,22 @@ namespace Avalonia.Controls.Platform } } + protected internal virtual void PointerMoved(object? sender, PointerEventArgs e) + { + // HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method. + var item = GetMenuItem(e.Source as IControl) as MenuItem; + if (item?.TransformedBounds == null) + { + return; + } + var point = e.GetCurrentPoint(null); + + if (point.Properties.IsLeftButtonPressed && item.TransformedBounds.Value.Contains(point.Position) == false) + { + e.Pointer.Capture(null); + } + } + protected internal virtual void PointerLeave(object? sender, PointerEventArgs e) { var item = GetMenuItem(e.Source as IControl); diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 4a9e745e30..093f10be51 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls /// Gets or sets a collection of filters which determine the types of files displayed in an /// or an . /// - public List Filters { get; set; } = new List(); + public List? Filters { get; set; } = new List(); /// /// Gets or sets initial file name that is displayed when the dialog is opened. diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 4372829df1..d1d9c17ae3 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -30,7 +30,7 @@ namespace Avalonia.Native ofd.Title ?? "", ofd.Directory ?? "", ofd.InitialFileName ?? "", - string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); + string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty())); } else { @@ -39,7 +39,7 @@ namespace Avalonia.Native dialog.Title ?? "", dialog.Directory ?? "", dialog.InitialFileName ?? "", - string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); + string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty())); } return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index f545206a2f..a93fb6831d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -7,9 +7,11 @@ + 8,5,8,6 + - - + + - + - + - + - + - + + + + + - diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index f16e1ed99f..2d70a35b13 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -1,294 +1,321 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index 4c4df1f53a..70209fb3ad 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -151,6 +151,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers results.Add(result); result = initialNode; break; + case SelectorGrammar.NestingSyntax: + var parentTargetType = context.ParentNodes().OfType().FirstOrDefault(); + + if (parentTargetType is null) + throw new XamlParseException($"Cannot find parent style for nested selector.", node); + + result = new XamlIlNestingSelector(result, parentTargetType.TargetType.GetClrType()); + break; default: throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); } @@ -474,4 +482,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList")); } } + + class XamlIlNestingSelector : XamlIlSelectorNode + { + public XamlIlNestingSelector(XamlIlSelectorNode previous, IXamlType targetType) + : base(previous) + { + TargetType = targetType; + } + + public override IXamlType TargetType { get; } + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + EmitCall(context, codeGen, + m => m.Name == "Nesting" && m.Parameters.Count == 1); + } + } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index a9fc18474c..16856e674d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -46,7 +46,7 @@ namespace Avalonia.Markup.Parsers switch (state) { case State.Start: - state = ParseStart(ref r); + (state, syntax) = ParseStart(ref r); break; case State.Middle: (state, syntax) = ParseMiddle(ref r, end); @@ -93,27 +93,31 @@ namespace Avalonia.Markup.Parsers return selector; } - private static State ParseStart(ref CharacterReader r) + private static (State, ISyntax?) ParseStart(ref CharacterReader r) { r.SkipWhitespace(); if (r.End) { - return State.End; + return (State.End, null); } if (r.TakeIf(':')) { - return State.Colon; + return (State.Colon, null); } else if (r.TakeIf('.')) { - return State.Class; + return (State.Class, null); } else if (r.TakeIf('#')) { - return State.Name; + return (State.Name, null); + } + else if (r.TakeIf('^')) + { + return (State.CanHaveType, new NestingSyntax()); } - return State.TypeName; + return (State.TypeName, null); } private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) @@ -142,6 +146,10 @@ namespace Avalonia.Markup.Parsers { return (State.Start, new CommaSyntax()); } + else if (r.TakeIf('^')) + { + return (State.CanHaveType, new NestingSyntax()); + } else if (end.HasValue && !r.End && r.Peek == end.Value) { return (State.End, null); @@ -635,5 +643,13 @@ namespace Avalonia.Markup.Parsers return obj is CommaSyntax or; } } + + public class NestingSyntax : ISyntax + { + public override bool Equals(object? obj) + { + return obj is NestingSyntax; + } + } } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 166f2e8f98..d584216f17 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -103,9 +103,9 @@ namespace Avalonia.Skia SkewY = (float)m.M12, ScaleY = (float)m.M22, TransY = (float)m.M32, - Persp0 = 0, - Persp1 = 0, - Persp2 = 1 + Persp0 = (float)m.M13, + Persp1 = (float)m.M23, + Persp2 = (float)m.M33 }; return sm; diff --git a/tests/Avalonia.Base.UnitTests/MatrixTests.cs b/tests/Avalonia.Base.UnitTests/MatrixTests.cs new file mode 100644 index 0000000000..99fe4685d7 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/MatrixTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Visuals.UnitTests; + +/// +/// These tests use the "official" Matrix4x4 and Matrix3x2 from the System.Numerics namespace, to validate +/// that Avalonias own implementation of a 3x3 Matrix works correctly. +/// +public class MatrixTests +{ + /// + /// Because Avalonia is working internally with doubles, but System.Numerics Vector and Matrix implementations + /// only make use of floats, we need to reduce precision, comparing them. It should be sufficient to compare + /// 5 fractional digits to ensure, that the result is correct. + /// + /// The expected vector + /// The actual transformed point + private void AssertCoordinatesEqualWithReducedPrecision(Vector2 expected, Point actual) + { + double ReducePrecision(double input) => Math.Truncate(input * 10000); + + var expectedX = ReducePrecision(expected.X); + var expectedY = ReducePrecision(expected.Y); + + var actualX = ReducePrecision(actual.X); + var actualY = ReducePrecision(actual.Y); + + Assert.Equal(expectedX, actualX); + Assert.Equal(expectedY, actualY); + } + + [Fact] + public void Transform_Point_Should_Return_Correct_Value_For_Translated_Matrix() + { + var vector2 = Vector2.Transform( + new Vector2(1, 1), + Matrix3x2.CreateTranslation(2, 2)); + var expected = new Point(vector2.X, vector2.Y); + + var matrix = Matrix.CreateTranslation(2, 2); + var point = new Point(1, 1); + var transformedPoint = matrix.Transform(point); + + Assert.Equal(expected, transformedPoint); + } + + [Fact] + public void Transform_Point_Should_Return_Correct_Value_For_Rotated_Matrix() + { + var expected = Vector2.Transform( + new Vector2(0, 10), + Matrix3x2.CreateRotation((float)Matrix.ToRadians(45))); + + var matrix = Matrix.CreateRotation(Matrix.ToRadians(45)); + var point = new Point(0, 10); + var actual = matrix.Transform(point); + + AssertCoordinatesEqualWithReducedPrecision(expected, actual); + } + + [Fact] + public void Transform_Point_Should_Return_Correct_Value_For_Scaled_Matrix() + { + var vector2 = Vector2.Transform( + new Vector2(1, 1), + Matrix3x2.CreateScale(2, 2)); + var expected = new Point(vector2.X, vector2.Y); + var matrix = Matrix.CreateScale(2, 2); + var point = new Point(1, 1); + var actual = matrix.Transform(point); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Transform_Point_Should_Return_Correct_Value_For_Skewed_Matrix() + { + var expected = Vector2.Transform( + new Vector2(1, 1), + Matrix3x2.CreateSkew(30, 20)); + + var matrix = Matrix.CreateSkew(30, 20); + var point = new Point(1, 1); + var actual = matrix.Transform(point); + + AssertCoordinatesEqualWithReducedPrecision(expected, actual); + } +} diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs new file mode 100644 index 0000000000..d49fcf03a2 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -0,0 +1,275 @@ +using System; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Styling.Activators; +using Xunit; + +namespace Avalonia.Base.UnitTests.Styling +{ + public class SelectorTests_Nesting + { + [Fact] + public void Nesting_Class_Doesnt_Match_Parent_OfType_Selector() + { + var control = new Control2(); + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => x.Nesting().Class("foo"))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); + } + + [Fact] + public void Or_Nesting_Class_Doesnt_Match_Parent_OfType_Selector() + { + var control = new Control2(); + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => Selectors.Or( + x.Nesting().Class("foo"), + x.Nesting().Class("bar")))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); + } + + [Fact] + public void Or_Nesting_Child_OfType_Doesnt_Match_Parent_OfType_Selector() + { + var control = new Control1(); + var panel = new DockPanel { Children = { control } }; + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => Selectors.Or( + x.Nesting().Child().OfType(), + x.Nesting().Child().OfType()))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.NeverThisInstance, match.Result); + } + + [Fact] + public void Double_Nesting_Class_Doesnt_Match_Grandparent_OfType_Selector() + { + var control = new Control2 + { + Classes = { "foo", "bar" }, + }; + + Style parent; + Style nested; + var grandparent = new Style(x => x.OfType()) + { + Children = + { + (parent = new Style(x => x.Nesting().Class("foo")) + { + Children = + { + (nested = new Style(x => x.Nesting().Class("bar"))) + } + }) + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); + } + + [Fact] + public void Nesting_Class_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => x.Nesting().Class("foo"))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + + var sink = new ActivatorSink(match.Activator); + + Assert.True(sink.Active); + control.Classes.Clear(); + Assert.False(sink.Active); + } + + [Fact] + public void Double_Nesting_Class_Matches() + { + var control = new Control1 + { + Classes = { "foo", "bar" }, + }; + + Style parent; + Style nested; + var grandparent = new Style(x => x.OfType()) + { + Children = + { + (parent = new Style(x => x.Nesting().Class("foo")) + { + Children = + { + (nested = new Style(x => x.Nesting().Class("bar"))) + } + }) + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + + var sink = new ActivatorSink(match.Activator); + + Assert.True(sink.Active); + control.Classes.Remove("foo"); + Assert.False(sink.Active); + } + + [Fact] + public void Or_Nesting_Class_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => Selectors.Or( + x.Nesting().Class("foo"), + x.Nesting().Class("bar")))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + + var sink = new ActivatorSink(match.Activator); + + Assert.True(sink.Active); + control.Classes.Clear(); + Assert.False(sink.Active); + } + + [Fact] + public void Or_Nesting_Child_OfType_Matches() + { + var control = new Control1 { Classes = { "foo" } }; + var panel = new Panel { Children = { control } }; + Style nested; + var parent = new Style(x => x.OfType()) + { + Children = + { + (nested = new Style(x => Selectors.Or( + x.Nesting().Child().OfType(), + x.Nesting().Child().OfType()))), + } + }; + + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, match.Result); + } + + [Fact] + public void Nesting_With_No_Parent_Style_Fails() + { + var control = new Control1(); + var style = new Style(x => x.Nesting().OfType()); + + Assert.Throws(() => style.Selector.Match(control, null)); + } + + [Fact] + public void Nesting_With_No_Parent_Selector_Fails() + { + var control = new Control1(); + Style nested; + var parent = new Style + { + Children = + { + (nested = new Style(x => x.Nesting().Class("foo"))), + } + }; + + Assert.Throws(() => nested.Selector.Match(control, parent)); + } + + [Fact] + public void Adding_Child_With_No_Nesting_Selector_Fails() + { + var parent = new Style(x => x.OfType()); + var child = new Style(x => x.Class("foo")); + + Assert.Throws(() => parent.Children.Add(child)); + } + + [Fact] + public void Adding_Combinator_Selector_Child_With_No_Nesting_Selector_Fails() + { + var parent = new Style(x => x.OfType()); + var child = new Style(x => x.Class("foo").Descendant().Class("bar")); + + Assert.Throws(() => parent.Children.Add(child)); + } + + [Fact] + public void Adding_Or_Selector_Child_With_No_Nesting_Selector_Fails() + { + var parent = new Style(x => x.OfType()); + var child = new Style(x => Selectors.Or( + x.Nesting().Class("foo"), + x.Class("bar"))); + + Assert.Throws(() => parent.Children.Add(child)); + } + + [Fact] + public void Can_Add_Child_Without_Nesting_Selector_To_Style_Without_Selector() + { + var parent = new Style(); + var child = new Style(x => x.Class("foo")); + + parent.Children.Add(child); + } + + public class Control1 : Control + { + } + + public class Control2 : Control + { + } + + private class ActivatorSink : IStyleActivatorSink + { + public ActivatorSink(IStyleActivator source) => source.Subscribe(this); + public bool Active { get; private set; } + public void OnNext(bool value, int tag) => Active = value; + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs index 8dedf3471f..7aa86a1328 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs @@ -722,6 +722,48 @@ namespace Avalonia.Base.UnitTests.Styling resources.Verify(x => x.AddOwner(host.Object), Times.Once); } + [Fact] + public void Nested_Style_Can_Be_Added() + { + var parent = new Style(x => x.OfType()); + var nested = new Style(x => x.Nesting().Class("foo")); + + parent.Children.Add(nested); + + Assert.Same(parent, nested.Parent); + } + + [Fact] + public void Nested_Or_Style_Can_Be_Added() + { + var parent = new Style(x => x.OfType()); + var nested = new Style(x => Selectors.Or( + x.Nesting().Class("foo"), + x.Nesting().Class("bar"))); + + parent.Children.Add(nested); + + Assert.Same(parent, nested.Parent); + } + + [Fact] + public void Nested_Style_Without_Selector_Throws() + { + var parent = new Style(x => x.OfType()); + var nested = new Style(); + + Assert.Throws(() => parent.Children.Add(nested)); + } + + [Fact(Skip = "TODO")] + public void Nested_Style_Without_Nesting_Operator_Throws() + { + var parent = new Style(x => x.OfType()); + var nested = new Style(x => x.Class("foo")); + + Assert.Throws(() => parent.Children.Add(nested)); + } + private class Class1 : Control { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 6fbf024ff1..b41f37eb3d 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -469,6 +469,144 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void Nesting_Class() + { + var result = SelectorGrammar.Parse("^.foo"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void Nesting_Child_Class() + { + var result = SelectorGrammar.Parse("^ > .foo"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.ChildSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void Nesting_Descendant_Class() + { + var result = SelectorGrammar.Parse("^ .foo"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.DescendantSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void Nesting_Template_Class() + { + var result = SelectorGrammar.Parse("^ /template/ .foo"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.TemplateSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void OfType_Template_Nesting() + { + var result = SelectorGrammar.Parse("Button /template/ ^"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.TemplateSyntax { }, + new SelectorGrammar.NestingSyntax(), + }, + result); + } + + [Fact] + public void Nesting_Property() + { + var result = SelectorGrammar.Parse("^[Foo=bar]"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.PropertySyntax { Property = "Foo", Value = "bar" }, + }, + result); + } + + [Fact] + public void Not_Nesting() + { + var result = SelectorGrammar.Parse(":not(^)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NotSyntax + { + Argument = new[] { new SelectorGrammar.NestingSyntax() }, + } + }, + result); + } + + [Fact] + public void Nesting_NthChild() + { + var result = SelectorGrammar.Parse("^:nth-child(2n+1)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void Nesting_Comma_Nesting_Class() + { + var result = SelectorGrammar.Parse("^, ^.foo"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.CommaSyntax(), + new SelectorGrammar.NestingSyntax(), + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + [Fact] public void Namespace_Alone_Fails() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 022ff0c3a4..bdd5cbbe2b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -617,5 +617,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color); } } + + [Fact] + public void Can_Use_Nested_Styles() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var foo = window.FindControl("foo"); + + Assert.Null(foo.Background); + + foo.Classes.Add("foo"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color); + } + } } }