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 @@
+
+
+
+
+
+
+ I'm a text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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);
+ }
+ }
}
}