Browse Source

Merge branch 'master' into fixes/viewboxContainerBugs

pull/8174/head
Jumar Macato 4 years ago
committed by GitHub
parent
commit
34fd5e4eda
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      build/HarfBuzzSharp.props
  2. 6
      build/SkiaSharp.props
  3. 34
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  4. 8
      native/Avalonia.Native/src/OSX/PopupImpl.mm
  5. 41
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  6. 10
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  7. 5
      native/Avalonia.Native/src/OSX/WindowImpl.h
  8. 32
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  9. 4
      native/Avalonia.Native/src/OSX/WindowProtocol.h
  10. 3
      samples/ControlCatalog/ControlCatalog.csproj
  11. 1
      samples/ControlCatalog/Pages/CarouselPage.xaml
  12. 3
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  13. 3
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  14. 3
      samples/RenderDemo/MainWindow.xaml
  15. 210
      samples/RenderDemo/Pages/Transform3DPage.axaml
  16. 21
      samples/RenderDemo/Pages/Transform3DPage.axaml.cs
  17. 55
      samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs
  18. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  19. 1
      src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
  20. 6
      src/Avalonia.Base/Animation/PageSlide.cs
  21. 121
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  22. 2
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  23. 268
      src/Avalonia.Base/Matrix.cs
  24. 21
      src/Avalonia.Base/Point.cs
  25. 210
      src/Avalonia.Base/Rotate3DTransform.cs
  26. 5
      src/Avalonia.Base/Styling/ChildSelector.cs
  27. 5
      src/Avalonia.Base/Styling/DescendentSelector.cs
  28. 30
      src/Avalonia.Base/Styling/NestingSelector.cs
  29. 5
      src/Avalonia.Base/Styling/NotSelector.cs
  30. 3
      src/Avalonia.Base/Styling/NthChildSelector.cs
  31. 17
      src/Avalonia.Base/Styling/OrSelector.cs
  32. 3
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  33. 24
      src/Avalonia.Base/Styling/Selector.cs
  34. 5
      src/Avalonia.Base/Styling/Selectors.cs
  35. 46
      src/Avalonia.Base/Styling/Style.cs
  36. 58
      src/Avalonia.Base/Styling/StyleCache.cs
  37. 35
      src/Avalonia.Base/Styling/StyleChildren.cs
  38. 41
      src/Avalonia.Base/Styling/Styles.cs
  39. 5
      src/Avalonia.Base/Styling/TemplateSelector.cs
  40. 3
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  41. 1
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  42. 3
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  43. 18
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  44. 2
      src/Avalonia.Controls/SystemDialog.cs
  45. 4
      src/Avalonia.Native/SystemDialogs.cs
  46. 80
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  47. 603
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  48. 24
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  49. 30
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  50. 6
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  51. 92
      tests/Avalonia.Base.UnitTests/MatrixTests.cs
  52. 275
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
  53. 42
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  54. 138
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  55. 32
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

6
build/HarfBuzzSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.254" /> <PackageReference Include="HarfBuzzSharp" Version="2.8.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2-preview.254" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2-preview.254"/> <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

6
build/SkiaSharp.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.254" /> <PackageReference Include="SkiaSharp" Version="2.88.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0-preview.254" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.254"/> <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

34
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -31,6 +31,7 @@
ComPtr<WindowBaseImpl> _parent; ComPtr<WindowBaseImpl> _parent;
bool _closed; bool _closed;
bool _isEnabled; bool _isEnabled;
bool _canBecomeKeyWindow;
bool _isExtended; bool _isExtended;
AvnMenu* _menu; AvnMenu* _menu;
} }
@ -216,29 +217,38 @@
-(BOOL)canBecomeKeyWindow -(BOOL)canBecomeKeyWindow
{ {
// If the window has a child window being shown as a dialog then don't allow it to become the key window. if(_canBecomeKeyWindow)
for(NSWindow* uch in [self childWindows])
{ {
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 <AvnWindowProtocol> ch = (id <AvnWindowProtocol>) uch; id <AvnWindowProtocol> ch = (id <AvnWindowProtocol>) uch;
return !ch.isDialog; if(ch.isDialog)
} return false;
}
return true; return true;
}
return false;
} }
#ifndef IS_NSPANEL
-(BOOL)canBecomeMainWindow -(BOOL)canBecomeMainWindow
{ {
#ifdef IS_NSPANEL
return false;
#else
return true; return true;
}
#endif #endif
-(void)setCanBecomeKeyWindow:(bool)value
{
_canBecomeKeyWindow = value;
} }
-(bool)shouldTryToHandleEvents -(bool)shouldTryToHandleEvents

8
native/Avalonia.Native/src/OSX/PopupImpl.mm

@ -26,13 +26,17 @@ private:
PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
{ {
WindowEvents = events; WindowEvents = events;
[Window setLevel:NSPopUpMenuWindowLevel];
} }
protected: protected:
virtual NSWindowStyleMask GetStyle() override virtual NSWindowStyleMask GetStyle() override
{ {
return NSWindowStyleMaskBorderless; return NSWindowStyleMaskBorderless;
} }
virtual void OnInitialiseNSWindow () override
{
[Window setLevel:NSPopUpMenuWindowLevel];
}
public: public:
virtual bool ShouldTakeFocusOnShow() override virtual bool ShouldTakeFocusOnShow() override
@ -54,4 +58,4 @@ extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl)
IAvnPopup* ptr = dynamic_cast<IAvnPopup*>(new PopupImpl(events, gl)); IAvnPopup* ptr = dynamic_cast<IAvnPopup*>(new PopupImpl(events, gl));
return ptr; return ptr;
} }
} }

41
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -16,8 +16,6 @@
class WindowBaseImpl : public virtual ComObject, class WindowBaseImpl : public virtual ComObject,
public virtual IAvnWindowBase, public virtual IAvnWindowBase,
public INSWindowHolder { public INSWindowHolder {
private:
NSCursor *cursor;
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
@ -28,23 +26,6 @@ BEGIN_INTERFACE_MAP()
virtual ~WindowBaseImpl(); virtual ~WindowBaseImpl();
AutoFitContentView *StandardContainer;
AvnView *View;
NSWindow * Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents;
ComPtr<IAvnGlContext> _glContext;
NSObject <IRenderTarget> *renderTarget;
AvnPoint lastPositionSet;
bool hasPosition;
NSSize lastSize;
NSSize lastMinSize;
NSSize lastMaxSize;
AvnMenu* lastMenu;
NSString *_lastTitle;
bool _shown;
bool _inResize;
WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl); WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl);
virtual HRESULT ObtainNSWindowHandle(void **ret) override; virtual HRESULT ObtainNSWindowHandle(void **ret) override;
@ -123,11 +104,33 @@ protected:
virtual NSWindowStyleMask GetStyle(); virtual NSWindowStyleMask GetStyle();
void UpdateStyle(); void UpdateStyle();
virtual void OnInitialiseNSWindow ();
private: private:
void CreateNSWindow (bool isDialog); void CreateNSWindow (bool isDialog);
void CleanNSWindow (); void CleanNSWindow ();
void InitialiseNSWindow (); void InitialiseNSWindow ();
NSCursor *cursor;
ComPtr<IAvnGlContext> _glContext;
bool hasPosition;
NSSize lastSize;
NSSize lastMinSize;
NSSize lastMaxSize;
AvnMenu* lastMenu;
bool _inResize;
protected:
AvnPoint lastPositionSet;
AutoFitContentView *StandardContainer;
bool _shown;
public:
NSObject <IRenderTarget> *renderTarget;
NSWindow * Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents;
AvnView *View;
}; };
#endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H #endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H

10
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -35,7 +35,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl)
lastSize = NSSize { 100, 100 }; lastSize = NSSize { 100, 100 };
lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX};
lastMinSize = NSSize { 0, 0 }; lastMinSize = NSSize { 0, 0 };
_lastTitle = @"";
Window = nullptr; Window = nullptr;
lastMenu = nullptr; lastMenu = nullptr;
@ -102,8 +101,6 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) {
UpdateStyle(); UpdateStyle();
[Window setTitle:_lastTitle];
if (ShouldTakeFocusOnShow() && activate) { if (ShouldTakeFocusOnShow() && activate) {
[Window orderFront:Window]; [Window orderFront:Window];
[Window makeKeyAndOrderFront:Window]; [Window makeKeyAndOrderFront:Window];
@ -570,6 +567,11 @@ void WindowBaseImpl::CreateNSWindow(bool isDialog) {
} }
} }
void WindowBaseImpl::OnInitialiseNSWindow()
{
}
void WindowBaseImpl::InitialiseNSWindow() { void WindowBaseImpl::InitialiseNSWindow() {
if(Window != nullptr) { if(Window != nullptr) {
[Window setContentView:StandardContainer]; [Window setContentView:StandardContainer];
@ -589,6 +591,8 @@ void WindowBaseImpl::InitialiseNSWindow() {
[GetWindowProtocol() showWindowMenuWithAppMenu]; [GetWindowProtocol() showWindowMenuWithAppMenu];
} }
} }
OnInitialiseNSWindow();
} }
} }

5
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -88,9 +88,14 @@ BEGIN_INTERFACE_MAP()
virtual HRESULT SetWindowState (AvnWindowState state) override; virtual HRESULT SetWindowState (AvnWindowState state) override;
virtual bool IsDialog() override; virtual bool IsDialog() override;
virtual void OnInitialiseNSWindow() override;
protected: protected:
virtual NSWindowStyleMask GetStyle() override; virtual NSWindowStyleMask GetStyle() override;
private:
NSString *_lastTitle;
}; };
#endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H #endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H

32
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -19,10 +19,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase
_inSetWindowState = false; _inSetWindowState = false;
_lastWindowState = Normal; _lastWindowState = Normal;
_actualWindowState = Normal; _actualWindowState = Normal;
_lastTitle = @"";
WindowEvents = events; WindowEvents = events;
[Window disableCursorRects];
[Window setTabbingMode:NSWindowTabbingModeDisallowed];
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
} }
void WindowImpl::HideOrShowTrafficLights() { 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) { HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL; START_COM_CALL;
@autoreleasepool { @autoreleasepool {
_isDialog = isDialog; _isDialog = isDialog;
bool created = Window == nullptr;
WindowBaseImpl::Show(activate, isDialog); WindowBaseImpl::Show(activate, isDialog);
if(created)
{
if(_isClientAreaExtended)
{
[GetWindowProtocol() setIsExtended:true];
SetExtendClientArea(true);
}
}
HideOrShowTrafficLights(); HideOrShowTrafficLights();
return SetWindowState(_lastWindowState); return SetWindowState(_lastWindowState);
@ -521,7 +523,7 @@ bool WindowImpl::IsDialog() {
} }
NSWindowStyleMask WindowImpl::GetStyle() { NSWindowStyleMask WindowImpl::GetStyle() {
unsigned long s = this->_isDialog ? NSWindowStyleMaskUtilityWindow : NSWindowStyleMaskBorderless; unsigned long s = this->_isDialog ? NSWindowStyleMaskDocModalWindow : NSWindowStyleMaskBorderless;
switch (_decorations) { switch (_decorations) {
case SystemDecorationsNone: case SystemDecorationsNone:

4
native/Avalonia.Native/src/OSX/WindowProtocol.h

@ -22,4 +22,6 @@
-(void) setIsExtended:(bool)value; -(void) setIsExtended:(bool)value;
-(void) disconnectParent; -(void) disconnectParent;
-(bool) isDialog; -(bool) isDialog;
@end
-(void) setCanBecomeKeyWindow:(bool)value;
@end

3
samples/ControlCatalog/ControlCatalog.csproj

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Update="**\*.xaml.cs"> <Compile Update="**\*.xaml.cs">

1
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -29,6 +29,7 @@
<ComboBoxItem>None</ComboBoxItem> <ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Slide</ComboBoxItem> <ComboBoxItem>Slide</ComboBoxItem>
<ComboBoxItem>Crossfade</ComboBoxItem> <ComboBoxItem>Crossfade</ComboBoxItem>
<ComboBoxItem>3D Rotation</ComboBoxItem>
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>

3
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@ -45,6 +45,9 @@ namespace ControlCatalog.Pages
case 2: case 2:
_carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25)); _carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break; break;
case 3:
_carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
break;
} }
} }
} }

3
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -8,7 +8,6 @@ using Avalonia.Dialogs;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
#pragma warning disable 4014 #pragma warning disable 4014
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
public class DialogsPage : UserControl public class DialogsPage : UserControl
@ -22,7 +21,7 @@ namespace ControlCatalog.Pages
string lastSelectedDirectory = null; string lastSelectedDirectory = null;
List<FileDialogFilter> GetFilters() List<FileDialogFilter>? GetFilters()
{ {
if (this.FindControl<CheckBox>("UseFilters").IsChecked != true) if (this.FindControl<CheckBox>("UseFilters").IsChecked != true)
return null; return null;

3
samples/RenderDemo/MainWindow.xaml

@ -72,5 +72,8 @@
<TabItem Header="Brushes"> <TabItem Header="Brushes">
<pages:BrushesPage /> <pages:BrushesPage />
</TabItem> </TabItem>
<TabItem Header="3D Transformation">
<pages:Transform3DPage />
</TabItem>
</controls:HamburgerMenu> </controls:HamburgerMenu>
</Window> </Window>

210
samples/RenderDemo/Pages/Transform3DPage.axaml

@ -0,0 +1,210 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="700"
x:Class="RenderDemo.Pages.Transform3DPage">
<UserControl.Styles>
<Styles>
<Styles.Resources>
<Template x:Key="TestContent">
<Grid RowDefinitions="*,*" ColumnDefinitions="*,*" Margin="5">
<TextBlock>I'm a text</TextBlock>
<Button Grid.Row="0" Grid.Column="1" Content="A Button"></Button>
<Slider Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Value="{Binding Depth}"
Minimum="100"
Maximum="300" />
</Grid>
</Template>
</Styles.Resources>
</Styles>
<Style Selector="Border.Test">
<Setter Property="Width" Value="200" />
<Setter Property="Height" Value="200" />
<Setter Property="Child" Value="{StaticResource TestContent}" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<Style Selector="TextBlock, Label, Slider">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="10,0,10,0" />
</Style>
<Style Selector="Border TextBlock">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Border Button">
<Setter Property="Background" Value="White"></Setter>
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="Border#B1">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="0" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Rotate3DTransform.AngleX" Value="90" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border#B2">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="90" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Rotate3DTransform.AngleX" Value="180" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="75%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="450" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border#B3">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="180" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="75%">
<Setter Property="Rotate3DTransform.AngleX" Value="450" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="540" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border#B4">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="270" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Rotate3DTransform.AngleX" Value="450" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="630" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="*, Auto, Auto, Auto, Auto, Auto, Auto, Auto">
<Grid.Clock>
<Clock />
</Grid.Clock>
<Border Name="B1" Background="DarkRed" Classes="Test">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Border Name="B2" Grid.Row="0" Grid.Column="0" Classes="Test" Background="DarkGreen">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Border Name="B3" Grid.Row="0" Grid.Column="0" Classes="Test" Background="DarkBlue">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Border Name="B4" Grid.Row="0" Grid.Column="0" Classes="Test" Background="Orange">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Label Grid.Column="0" Grid.Row="1">Depth: </Label>
<Slider Grid.Column="1" Grid.Row="1" Value="{Binding Depth}" Minimum="100" Maximum="300" />
<Border Grid.Row="0" Grid.Column="2" Classes="Test" ZIndex="-2">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="Red" />
<GradientStop Offset="1" Color="Blue" />
</LinearGradientBrush>
</Border.Background>
<Border.Styles>
<Style Selector="Label">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Slider">
<Setter Property="Width" Value="100" />
</Style>
</Border.Styles>
<Border.RenderTransform>
<Rotate3DTransform Depth="{Binding Depth}"
CenterX="{Binding CenterX}"
CenterY="{Binding CenterY}"
CenterZ="{Binding CenterZ}"
AngleX="{Binding AngleX}"
AngleY="{Binding AngleY}"
AngleZ="{Binding AngleZ}" />
</Border.RenderTransform>
</Border>
<Label Grid.Row="1" Grid.Column="2">Center X: </Label>
<Slider Grid.Row="1" Grid.Column="3" Value="{Binding CenterX}" Minimum="-100" Maximum="100" />
<Label Grid.Row="2" Grid.Column="2">Center Y: </Label>
<Slider Grid.Row="2" Grid.Column="3" Value="{Binding CenterY}" Minimum="-100" Maximum="100" />
<Label Grid.Row="3" Grid.Column="2">Center Z: </Label>
<Slider Grid.Row="3" Grid.Column="3" Value="{Binding CenterZ}" Minimum="-100" Maximum="100" />
<Label Grid.Row="4" Grid.Column="2">Angle X: </Label>
<Slider Grid.Row="4" Grid.Column="3" Value="{Binding AngleX}" Minimum="-180" Maximum="180" />
<Label Grid.Row="5" Grid.Column="2">Angle Y: </Label>
<Slider Grid.Row="5" Grid.Column="3" Value="{Binding AngleY}" Minimum="-180" Maximum="180" />
<Label Grid.Row="6" Grid.Column="2">Angle Z: </Label>
<Slider Grid.Row="6" Grid.Column="3" Value="{Binding AngleZ}" Minimum="-180" Maximum="180" />
</Grid>
</UserControl>

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

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

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -40,7 +40,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_gl = GlPlatformSurface.TryCreate(this); _gl = GlPlatformSurface.TryCreate(this);
_framebuffer = new FramebufferManager(this); _framebuffer = new FramebufferManager(this);
RenderScaling = (int)_view.Scaling; RenderScaling = _view.Scaling;
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);

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

@ -43,6 +43,7 @@ namespace Avalonia.Animation.Animators
normalTransform.Children.Add(new SkewTransform()); normalTransform.Children.Add(new SkewTransform());
normalTransform.Children.Add(new RotateTransform()); normalTransform.Children.Add(new RotateTransform());
normalTransform.Children.Add(new TranslateTransform()); normalTransform.Children.Add(new TranslateTransform());
normalTransform.Children.Add(new Rotate3DTransform());
ctrl.RenderTransform = normalTransform; ctrl.RenderTransform = normalTransform;
} }

6
src/Avalonia.Base/Animation/PageSlide.cs

@ -10,7 +10,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Transitions between two pages by sliding them horizontally. /// Transitions between two pages by sliding them horizontally or vertically.
/// </summary> /// </summary>
public class PageSlide : IPageTransition public class PageSlide : IPageTransition
{ {
@ -62,7 +62,7 @@ namespace Avalonia.Animation
public Easing SlideOutEasing { get; set; } = new LinearEasing(); public Easing SlideOutEasing { get; set; } = new LinearEasing();
/// <inheritdoc /> /// <inheritdoc />
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) if (cancellationToken.IsCancellationRequested)
{ {
@ -157,7 +157,7 @@ namespace Avalonia.Animation
/// <remarks> /// <remarks>
/// Any one of the parameters may be null, but not both. /// Any one of the parameters may be null, but not both.
/// </remarks> /// </remarks>
private static IVisual GetVisualParent(IVisual? from, IVisual? to) protected static IVisual GetVisualParent(IVisual? from, IVisual? to)
{ {
var p1 = (from ?? to)!.VisualParent; var p1 = (from ?? to)!.VisualParent;
var p2 = (to ?? from)!.VisualParent; var p2 = (to ?? from)!.VisualParent;

121
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
{
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// </summary>
/// <param name="duration">How long the rotation should take place</param>
/// <param name="orientation">The orientation of the rotation</param>
/// <param name="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</param>
public Rotate3DTransition(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal, double? depth = null)
: base(duration, orientation)
{
Depth = depth;
}
/// <summary>
/// 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.
/// </summary>
public double? Depth { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// </summary>
public Rotate3DTransition() { }
/// <inheritdoc />
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;
}
}
}
}

2
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -55,7 +55,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This will usually be true, except in /// This will usually be true, except in
/// <see cref="AvaloniaObject.OnPropertyChangedCore{T}(AvaloniaPropertyChangedEventArgs{T})"/> /// <see cref="AvaloniaObject.OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs)"/>
/// which receives notifications for all changes to property values, whether a value with a higher /// 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 /// 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. /// has not resulted in a change to the property value on the object.

268
src/Avalonia.Base/Matrix.cs

@ -1,12 +1,22 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Numerics;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
/// A 2x3 matrix. /// A 3x3 matrix.
/// </summary> /// </summary>
/// <remakrs>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).
/// </remakrs>
#if !BUILDTASK #if !BUILDTASK
public public
#endif #endif
@ -14,40 +24,76 @@ namespace Avalonia
{ {
private readonly double _m11; private readonly double _m11;
private readonly double _m12; private readonly double _m12;
private readonly double _m13;
private readonly double _m21; private readonly double _m21;
private readonly double _m22; private readonly double _m22;
private readonly double _m23;
private readonly double _m31; private readonly double _m31;
private readonly double _m32; private readonly double _m32;
private readonly double _m33;
/// <summary>
/// Initializes a new instance of the <see cref="Matrix"/> struct (equivalent to a 2x3 Matrix without perspective).
/// </summary>
/// <param name="scaleX">The first element of the first row.</param>
/// <param name="skrewY">The second element of the first row.</param>
/// <param name="skrewX">The first element of the second row.</param>
/// <param name="scaleY">The second element of the second row.</param>
/// <param name="offsetX">The first element of the third row.</param>
/// <param name="offsetY">The second element of the third row.</param>
public Matrix(
double scaleX,
double skrewY,
double skrewX,
double scaleY,
double offsetX,
double offsetY) : this( scaleX, skrewY, 0, skrewX, scaleY, 0, offsetX, offsetY, 1)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Matrix"/> struct. /// Initializes a new instance of the <see cref="Matrix"/> struct.
/// </summary> /// </summary>
/// <param name="m11">The first element of the first row.</param> /// <param name="scaleX">The first element of the first row.</param>
/// <param name="m12">The second element of the first row.</param> /// <param name="skrewY">The second element of the first row.</param>
/// <param name="m21">The first element of the second row.</param> /// <param name="persX">The third element of the first row.</param>
/// <param name="m22">The second element of the second row.</param> /// <param name="skrewX">The first element of the second row.</param>
/// <param name="scaleY">The second element of the second row.</param>
/// <param name="persY">The third element of the second row.</param>
/// <param name="offsetX">The first element of the third row.</param> /// <param name="offsetX">The first element of the third row.</param>
/// <param name="offsetY">The second element of the third row.</param> /// <param name="offsetY">The second element of the third row.</param>
/// <param name="persZ">The third element of the third row.</param>
public Matrix( public Matrix(
double m11, double scaleX,
double m12, double skrewY,
double m21, double persX,
double m22, double skrewX,
double scaleY,
double persY,
double offsetX, double offsetX,
double offsetY) double offsetY,
double persZ)
{ {
_m11 = m11; _m11 = scaleX;
_m12 = m12; _m12 = skrewY;
_m21 = m21; _m13 = persX;
_m22 = m22; _m21 = skrewX;
_m22 = scaleY;
_m23 = persY;
_m31 = offsetX; _m31 = offsetX;
_m32 = offsetY; _m32 = offsetY;
_m33 = persZ;
} }
/// <summary> /// <summary>
/// Returns the multiplicative identity matrix. /// Returns the multiplicative identity matrix.
/// </summary> /// </summary>
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);
/// <summary> /// <summary>
/// Returns whether the matrix is the identity matrix. /// Returns whether the matrix is the identity matrix.
@ -60,35 +106,50 @@ namespace Avalonia
public bool HasInverse => !MathUtilities.IsZero(GetDeterminant()); public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
/// <summary> /// <summary>
/// The first element of the first row /// The first element of the first row (scaleX).
/// </summary> /// </summary>
public double M11 => _m11; public double M11 => _m11;
/// <summary> /// <summary>
/// The second element of the first row /// The second element of the first row (skrewY).
/// </summary> /// </summary>
public double M12 => _m12; public double M12 => _m12;
/// <summary> /// <summary>
/// The first element of the second row /// The third element of the first row (persX: input x-axis perspective factor).
/// </summary>
public double M13 => _m13;
/// <summary>
/// The first element of the second row (skrewX).
/// </summary> /// </summary>
public double M21 => _m21; public double M21 => _m21;
/// <summary> /// <summary>
/// The second element of the second row /// The second element of the second row (scaleY).
/// </summary> /// </summary>
public double M22 => _m22; public double M22 => _m22;
/// <summary> /// <summary>
/// The first element of the third row /// The third element of the second row (persY: input y-axis perspective factor).
/// </summary>
public double M23 => _m23;
/// <summary>
/// The first element of the third row (offsetX/translateX).
/// </summary> /// </summary>
public double M31 => _m31; public double M31 => _m31;
/// <summary> /// <summary>
/// The second element of the third row /// The second element of the third row (offsetY/translateY).
/// </summary> /// </summary>
public double M32 => _m32; public double M32 => _m32;
/// <summary>
/// The third element of the third row (persZ: perspective scale factor).
/// </summary>
public double M33 => _m33;
/// <summary> /// <summary>
/// Multiplies two matrices together and returns the resulting matrix. /// Multiplies two matrices together and returns the resulting matrix.
/// </summary> /// </summary>
@ -98,12 +159,15 @@ namespace Avalonia
public static Matrix operator *(Matrix value1, Matrix value2) public static Matrix operator *(Matrix value1, Matrix value2)
{ {
return new Matrix( return new Matrix(
(value1.M11 * value2.M11) + (value1.M12 * value2.M21), (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31),
(value1.M11 * value2.M12) + (value1.M12 * value2.M22), (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32),
(value1.M21 * value2.M11) + (value1.M22 * value2.M21), (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33),
(value1.M21 * value2.M12) + (value1.M22 * value2.M22), (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31),
(value1._m31 * value2.M11) + (value1._m32 * value2.M21) + value2._m31, (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32),
(value1._m31 * value2.M12) + (value1._m32 * value2.M22) + 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));
} }
/// <summary> /// <summary>
@ -171,7 +235,7 @@ namespace Avalonia
/// <returns>A scaling matrix.</returns> /// <returns>A scaling matrix.</returns>
public static Matrix CreateScale(double xScale, double yScale) public static Matrix CreateScale(double xScale, double yScale)
{ {
return CreateScale(new Vector(xScale, yScale)); return new Matrix(xScale, 0, 0, yScale, 0, 0);
} }
/// <summary> /// <summary>
@ -181,7 +245,7 @@ namespace Avalonia
/// <returns>A scaling matrix.</returns> /// <returns>A scaling matrix.</returns>
public static Matrix CreateScale(Vector scales) public static Matrix CreateScale(Vector scales)
{ {
return new Matrix(scales.X, 0, 0, scales.Y, 0, 0); return CreateScale(scales.X, scales.Y);
} }
/// <summary> /// <summary>
@ -214,7 +278,7 @@ namespace Avalonia
{ {
return angle * 0.0174532925; return angle * 0.0174532925;
} }
/// <summary> /// <summary>
/// Appends another matrix as post-multiplication operation. /// Appends another matrix as post-multiplication operation.
/// Equivalent to this * value; /// Equivalent to this * value;
@ -227,7 +291,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Prpends another matrix as pre-multiplication operation. /// Prepends another matrix as pre-multiplication operation.
/// Equivalent to value * this; /// Equivalent to value * this;
/// </summary> /// </summary>
/// <param name="value">A matrix.</param> /// <param name="value">A matrix.</param>
@ -247,7 +311,49 @@ namespace Avalonia
/// </remarks> /// </remarks>
public double GetDeterminant() 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);
}
/// <summary>
/// Transforms the point with the matrix
/// </summary>
/// <param name="p">The point to be transformed</param>
/// <returns>The transformed point</returns>
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;
} }
/// <summary> /// <summary>
@ -260,10 +366,13 @@ namespace Avalonia
// ReSharper disable CompareOfFloatsByEqualityOperator // ReSharper disable CompareOfFloatsByEqualityOperator
return _m11 == other.M11 && return _m11 == other.M11 &&
_m12 == other.M12 && _m12 == other.M12 &&
_m13 == other.M13 &&
_m21 == other.M21 && _m21 == other.M21 &&
_m22 == other.M22 && _m22 == other.M22 &&
_m23 == other.M23 &&
_m31 == other.M31 && _m31 == other.M31 &&
_m32 == other.M32; _m32 == other.M32 &&
_m33 == other.M33;
// ReSharper restore CompareOfFloatsByEqualityOperator // ReSharper restore CompareOfFloatsByEqualityOperator
} }
@ -280,9 +389,18 @@ namespace Avalonia
/// <returns>The hash code.</returns> /// <returns>The hash code.</returns>
public override int GetHashCode() public override int GetHashCode()
{ {
return M11.GetHashCode() + M12.GetHashCode() + return (_m11, _m12, _m13, _m21, _m22, _m23, _m31, _m32, _m33).GetHashCode();
M21.GetHashCode() + M22.GetHashCode() + }
M31.GetHashCode() + M32.GetHashCode();
/// <summary>
/// Determines if the current matrix contains perspective (non-affine) transforms (true) or only (affine) transforms that could be mapped into an 2x3 matrix (false).
/// </summary>
public bool ContainsPerspective()
{
// ReSharper disable CompareOfFloatsByEqualityOperator
return _m13 != 0 || _m23 != 0 || _m33 != 1;
// ReSharper restore CompareOfFloatsByEqualityOperator
} }
/// <summary> /// <summary>
@ -292,15 +410,25 @@ namespace Avalonia
public override string ToString() public override string ToString()
{ {
CultureInfo ci = CultureInfo.CurrentCulture; 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( return string.Format(
ci, ci,
"{{ {{M11:{0} M12:{1}}} {{M21:{2} M22:{3}}} {{M31:{4} M32:{5}}} }}", msg,
M11.ToString(ci), values.Select((v) => v.ToString(ci)).ToArray());
M12.ToString(ci),
M21.ToString(ci),
M22.ToString(ci),
M31.ToString(ci),
M32.ToString(ci));
} }
/// <summary> /// <summary>
@ -318,14 +446,20 @@ namespace Avalonia
return false; return false;
} }
var invdet = 1 / d;
inverted = new Matrix( inverted = new Matrix(
_m22 / d, (_m22 * _m33 - _m32 * _m23) * invdet,
-_m12 / d, (_m13 * _m31 - _m12 * _m33) * invdet,
-_m21 / d, (_m12 * _m23 - _m13 * _m22) * invdet,
_m11 / d, (_m23 * _m31 - _m21 * _m33) * invdet,
((_m21 * _m32) - (_m22 * _m31)) / d, (_m11 * _m33 - _m13 * _m31) * invdet,
((_m12 * _m31) - (_m11 * _m32)) / d); (_m21 * _m13 - _m11 * _m23) * invdet,
(_m21 * _m32 - _m31 * _m22) * invdet,
(_m21 * _m12 - _m11 * _m32) * invdet,
(_m11 * _m22 - _m21 * _m12) * invdet
);
return true; return true;
} }
@ -336,7 +470,7 @@ namespace Avalonia
/// <returns>The inverted matrix.</returns> /// <returns>The inverted matrix.</returns>
public Matrix Invert() public Matrix Invert()
{ {
if (!TryInvert(out Matrix inverted)) if (!TryInvert(out var inverted))
{ {
throw new InvalidOperationException("Transform is not invertible."); throw new InvalidOperationException("Transform is not invertible.");
} }
@ -347,20 +481,30 @@ namespace Avalonia
/// <summary> /// <summary>
/// Parses a <see cref="Matrix"/> string. /// Parses a <see cref="Matrix"/> string.
/// </summary> /// </summary>
/// <param name="s">Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new <see cref="Matrix"/></param> /// <param name="s">Six or nine comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY[, persX, persY, persZ]) that describe the new <see cref="Matrix"/></param>
/// <returns>The <see cref="Matrix"/>.</returns> /// <returns>The <see cref="Matrix"/>.</returns>
public static Matrix Parse(string s) 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.")) using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
{ {
return new Matrix( var v1 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v2 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v3 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v4 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v5 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v6 = tokenizer.ReadDouble();
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
/// </summary> /// </summary>
/// <param name="matrix">Matrix to decompose.</param> /// <param name="matrix">Matrix to decompose.</param>
/// <param name="decomposed">Decomposed matrix.</param> /// <param name="decomposed">Decomposed matrix.</param>
/// <returns>The status of the operation.</returns> /// <returns>The status of the operation.</returns>
public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
{ {
decomposed = default; decomposed = default;
var determinant = matrix.GetDeterminant(); var determinant = matrix.GetDeterminant();
if (MathUtilities.IsZero(determinant)) if (MathUtilities.IsZero(determinant) || matrix.ContainsPerspective())
{ {
return false; return false;
} }

21
src/Avalonia.Base/Point.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Numerics;
#if !BUILDTASK #if !BUILDTASK
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
#endif #endif
@ -168,12 +169,7 @@ namespace Avalonia
/// <param name="point">The point.</param> /// <param name="point">The point.</param>
/// <param name="matrix">The matrix.</param> /// <param name="matrix">The matrix.</param>
/// <returns>The resulting point.</returns> /// <returns>The resulting point.</returns>
public static Point operator *(Point point, Matrix matrix) public static Point operator *(Point point, Matrix matrix) => matrix.Transform(point);
{
return new Point(
(point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31,
(point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32);
}
/// <summary> /// <summary>
/// Parses a <see cref="Point"/> string. /// Parses a <see cref="Point"/> string.
@ -242,18 +238,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="transform">The transform.</param> /// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns> /// <returns>The transformed point.</returns>
public Point Transform(Matrix transform) public Point Transform(Matrix transform) => transform.Transform(this);
{
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);
}
/// <summary> /// <summary>
/// Returns a new point with the specified X coordinate. /// Returns a new point with the specified X coordinate.

210
src/Avalonia.Base/Rotate3DTransform.cs

@ -0,0 +1,210 @@
using System;
using System.Numerics;
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
/// <summary>
/// Non-Affine 3D transformation for rotating a visual around a definable axis
/// </summary>
public class Rotate3DTransform : Transform
{
private readonly bool _isInitializing;
/// <summary>
/// Defines the <see cref="AngleX"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleXProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(AngleX));
/// <summary>
/// Defines the <see cref="AngleY"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleYProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(AngleY));
/// <summary>
/// Defines the <see cref="AngleZ"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleZProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(AngleZ));
/// <summary>
/// Defines the <see cref="CenterX"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterXProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(CenterX));
/// <summary>
/// Defines the <see cref="CenterY"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterYProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(CenterY));
/// <summary>
/// Defines the <see cref="CenterZ"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterZProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(CenterZ));
/// <summary>
/// Defines the <see cref="Depth"/> property.
/// </summary>
public static readonly StyledProperty<double> DepthProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(Depth));
/// <summary>
/// Initializes a new instance of the <see cref="Rotate3DTransform"/> class.
/// </summary>
public Rotate3DTransform() { }
/// <summary>
/// Initializes a new instance of the <see cref="Rotate3DTransform"/> class.
/// </summary>
/// <param name="angleX">The rotation around the X-Axis</param>
/// <param name="angleY">The rotation around the Y-Axis</param>
/// <param name="angleZ">The rotation around the Z-Axis</param>
/// <param name="centerX">The origin of the X-Axis</param>
/// <param name="centerY">The origin of the Y-Axis</param>
/// <param name="centerZ">The origin of the Z-Axis</param>
/// <param name="depth">The depth of the 3D effect</param>
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;
}
/// <summary>
/// Sets the rotation around the X-Axis
/// </summary>
public double AngleX
{
get => GetValue(AngleXProperty);
set => SetValue(AngleXProperty, value);
}
/// <summary>
/// Sets the rotation around the Y-Axis
/// </summary>
public double AngleY
{
get => GetValue(AngleYProperty);
set => SetValue(AngleYProperty, value);
}
/// <summary>
/// Sets the rotation around the Z-Axis
/// </summary>
public double AngleZ
{
get => GetValue(AngleZProperty);
set => SetValue(AngleZProperty, value);
}
/// <summary>
/// Moves the origin the X-Axis rotates around
/// </summary>
public double CenterX
{
get => GetValue(CenterXProperty);
set => SetValue(CenterXProperty, value);
}
/// <summary>
/// Moves the origin the Y-Axis rotates around
/// </summary>
public double CenterY
{
get => GetValue(CenterYProperty);
set => SetValue(CenterYProperty, value);
}
/// <summary>
/// Moves the origin the Z-Axis rotates around
/// </summary>
public double CenterZ
{
get => GetValue(CenterZProperty);
set => SetValue(CenterZProperty, value);
}
/// <summary>
/// Affects the depth of the rotation effect
/// </summary>
public double Depth
{
get => GetValue(DepthProperty);
set => SetValue(DepthProperty, value);
}
/// <summary>
/// Gets the transform's <see cref="Matrix"/>.
/// </summary>
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();
}
}

5
src/Avalonia.Base/Styling/ChildSelector.cs

@ -37,13 +37,13 @@ namespace Avalonia.Styling
return _selectorString; 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; var controlParent = ((ILogical)control).LogicalParent;
if (controlParent != null) if (controlParent != null)
{ {
var parentMatch = _parent.Match((IStyleable)controlParent, subscribe); var parentMatch = _parent.Match((IStyleable)controlParent, parent, subscribe);
if (parentMatch.Result == SelectorMatchResult.Sometimes) if (parentMatch.Result == SelectorMatchResult.Sometimes)
{ {
@ -65,5 +65,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
} }
} }

5
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -35,7 +35,7 @@ namespace Avalonia.Styling
return _selectorString; 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 c = (ILogical)control;
var descendantMatches = new OrActivatorBuilder(); var descendantMatches = new OrActivatorBuilder();
@ -46,7 +46,7 @@ namespace Avalonia.Styling
if (c is IStyleable) if (c is IStyleable)
{ {
var match = _parent.Match((IStyleable)c, subscribe); var match = _parent.Match((IStyleable)c, parent, subscribe);
if (match.Result == SelectorMatchResult.Sometimes) if (match.Result == SelectorMatchResult.Sometimes)
{ {
@ -70,5 +70,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
} }
} }

30
src/Avalonia.Base/Styling/NestingSelector.cs

@ -0,0 +1,30 @@
using System;
namespace Avalonia.Styling
{
/// <summary>
/// The `^` nesting style selector.
/// </summary>
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;
}
}

5
src/Avalonia.Base/Styling/NotSelector.cs

@ -45,9 +45,9 @@ namespace Avalonia.Styling
return _selectorString; 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) switch (innerResult.Result)
{ {
@ -67,5 +67,6 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector();
} }
} }

3
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -48,7 +48,7 @@ namespace Avalonia.Styling
public int Step { get; } public int Step { get; }
public int Offset { 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)) if (!(control is ILogical logical))
{ {
@ -105,6 +105,7 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
public override string ToString() public override string ToString()
{ {

17
src/Avalonia.Base/Styling/OrSelector.cs

@ -65,14 +65,14 @@ namespace Avalonia.Styling
return _selectorString; 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 activators = new OrActivatorBuilder();
var neverThisInstance = false; var neverThisInstance = false;
foreach (var selector in _selectors) foreach (var selector in _selectors)
{ {
var match = selector.Match(control, subscribe); var match = selector.Match(control, parent, subscribe);
switch (match.Result) switch (match.Result)
{ {
@ -104,6 +104,19 @@ namespace Avalonia.Styling
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector()
{
foreach (var selector in _selectors)
{
if (!selector.HasValidNestingSelector())
{
return false;
}
}
return true;
}
private Type? EvaluateTargetType() private Type? EvaluateTargetType()
{ {
Type? result = null; Type? result = null;

3
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -74,7 +74,7 @@ namespace Avalonia.Styling
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{ {
if (subscribe) if (subscribe)
{ {
@ -90,6 +90,7 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
internal static bool Compare(Type propertyType, object? propertyValue, object? value) internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{ {

24
src/Avalonia.Base/Styling/Selector.cs

@ -33,22 +33,25 @@ namespace Avalonia.Styling
/// Tries to match the selector with a control. /// Tries to match the selector with a control.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
/// <param name="parent">
/// The parent style, if the style containing the selector is a nested style.
/// </param>
/// <param name="subscribe"> /// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time, /// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result. /// or simply return an immediate result.
/// </param> /// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns> /// <returns>A <see cref="SelectorMatch"/>.</returns>
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 // 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 // right-to-left, so MatchUntilCombinator reverses this order because the type selector
// will be on the left. // 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 the pre-combinator selector matches, we can now match the combinator, if any.
if (match.IsMatch && combinator is object) 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 // 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 // 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. /// Evaluates the selector for a match.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
/// <param name="parent">
/// The parent style, if the style containing the selector is a nested style.
/// </param>
/// <param name="subscribe"> /// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time, /// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result. /// or simply return an immediate result.
/// </param> /// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns> /// <returns>A <see cref="SelectorMatch"/>.</returns>
protected abstract SelectorMatch Evaluate(IStyleable control, bool subscribe); protected abstract SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe);
/// <summary> /// <summary>
/// Moves to the previous selector. /// Moves to the previous selector.
/// </summary> /// </summary>
protected abstract Selector? MovePrevious(); protected abstract Selector? MovePrevious();
internal abstract bool HasValidNestingSelector();
private static SelectorMatch MatchUntilCombinator( private static SelectorMatch MatchUntilCombinator(
IStyleable control, IStyleable control,
Selector start, Selector start,
IStyle? parent,
bool subscribe, bool subscribe,
out Selector? combinator) out Selector? combinator)
{ {
combinator = null; combinator = null;
var activators = new AndActivatorBuilder(); 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 ? return result == SelectorMatchResult.Sometimes ?
new SelectorMatch(activators.Get()) : new SelectorMatch(activators.Get()) :
@ -99,6 +108,7 @@ namespace Avalonia.Styling
private static SelectorMatchResult Match( private static SelectorMatchResult Match(
IStyleable control, IStyleable control,
Selector selector, Selector selector,
IStyle? parent,
bool subscribe, bool subscribe,
ref AndActivatorBuilder activators, ref AndActivatorBuilder activators,
ref Selector? combinator) ref Selector? combinator)
@ -110,7 +120,7 @@ namespace Avalonia.Styling
// opportunity to exit early. // opportunity to exit early.
if (previous != null && !previous.IsCombinator) 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) if (previousMatch < SelectorMatchResult.Sometimes)
{ {
@ -119,7 +129,7 @@ namespace Avalonia.Styling
} }
// Match this selector. // Match this selector.
var match = selector.Evaluate(control, subscribe); var match = selector.Evaluate(control, parent, subscribe);
if (!match.IsMatch) if (!match.IsMatch)
{ {

5
src/Avalonia.Base/Styling/Selectors.cs

@ -109,6 +109,11 @@ namespace Avalonia.Styling
} }
} }
public static Selector Nesting(this Selector? previous)
{
return new NestingSelector();
}
/// <summary> /// <summary>
/// Returns a selector which inverts the results of selector argument. /// Returns a selector which inverts the results of selector argument.
/// </summary> /// </summary>

46
src/Avalonia.Base/Styling/Style.cs

@ -4,8 +4,6 @@ using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Metadata; using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
@ -14,9 +12,11 @@ namespace Avalonia.Styling
public class Style : AvaloniaObject, IStyle, IResourceProvider public class Style : AvaloniaObject, IStyle, IResourceProvider
{ {
private IResourceHost? _owner; private IResourceHost? _owner;
private StyleChildren? _children;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private List<ISetter>? _setters; private List<ISetter>? _setters;
private List<IAnimation>? _animations; private List<IAnimation>? _animations;
private StyleCache? _childCache;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Style"/> class. /// Initializes a new instance of the <see cref="Style"/> class.
@ -34,6 +34,14 @@ namespace Avalonia.Styling
Selector = selector(null); Selector = selector(null);
} }
/// <summary>
/// Gets the children of the style.
/// </summary>
public IList<IStyle> Children => _children ??= new(this);
/// <summary>
/// Gets the <see cref="StyledElement"/> or Application that hosts the style.
/// </summary>
public IResourceHost? Owner public IResourceHost? Owner
{ {
get => _owner; get => _owner;
@ -47,6 +55,11 @@ namespace Avalonia.Styling
} }
} }
/// <summary>
/// Gets the parent style if this style is hosted in a <see cref="Style.Children"/> collection.
/// </summary>
public Style? Parent { get; private set; }
/// <summary> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
/// </summary> /// </summary>
@ -90,7 +103,7 @@ namespace Avalonia.Styling
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>(); public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
bool IResourceNode.HasResources => _resources?.Count > 0; bool IResourceNode.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>(); IReadOnlyList<IStyle> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>();
public event EventHandler? OwnerChanged; public event EventHandler? OwnerChanged;
@ -98,7 +111,7 @@ namespace Avalonia.Styling
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); 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; target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
if (match.IsMatch && (_setters is object || _animations is object)) if (match.IsMatch && (_setters is object || _animations is object))
@ -108,7 +121,17 @@ namespace Avalonia.Styling
instance.Start(); 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) public bool TryGetResource(object key, out object? result)
@ -156,5 +179,18 @@ namespace Avalonia.Styling
_resources?.RemoveOwner(owner); _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;
}
} }
} }

58
src/Avalonia.Base/Styling/StyleCache.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Styling
{
/// <summary>
/// Simple cache for improving performance of applying styles.
/// </summary>
/// <remarks>
/// Maps <see cref="IStyleable.StyleKey"/> to a list of styles that are known be be possible
/// matches.
/// </remarks>
internal class StyleCache : Dictionary<Type, List<IStyle>?>
{
public SelectorMatchResult TryAttach(IList<IStyle> 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<IStyle>? matches = null;
foreach (var child in styles)
{
if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType)
{
matches ??= new List<IStyle>();
matches.Add(child);
}
}
Add(target.StyleKey, matches);
return matches is null ?
SelectorMatchResult.NeverThisType :
SelectorMatchResult.AlwaysThisType;
}
}
}
}

35
src/Avalonia.Base/Styling/StyleChildren.cs

@ -0,0 +1,35 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
namespace Avalonia.Styling
{
internal class StyleChildren : Collection<IStyle>
{
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);
}
}
}

41
src/Avalonia.Base/Styling/Styles.cs

@ -20,7 +20,7 @@ namespace Avalonia.Styling
private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>(); private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private IResourceHost? _owner; private IResourceHost? _owner;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private Dictionary<Type, List<IStyle>?>? _cache; private StyleCache? _cache;
public Styles() public Styles()
{ {
@ -111,43 +111,8 @@ namespace Avalonia.Styling
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{ {
_cache ??= new Dictionary<Type, List<IStyle>?>(); _cache ??= new StyleCache();
return _cache.TryAttach(this, target, host);
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<IStyle>? matches = null;
foreach (var child in this)
{
if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType)
{
matches ??= new List<IStyle>();
matches.Add(child);
}
}
_cache.Add(target.StyleKey, matches);
return matches is null ?
SelectorMatchResult.NeverThisType :
SelectorMatchResult.AlwaysThisType;
}
} }
/// <inheritdoc/> /// <inheritdoc/>

5
src/Avalonia.Base/Styling/TemplateSelector.cs

@ -36,7 +36,7 @@ namespace Avalonia.Styling
return _selectorString; 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; var templatedParent = control.TemplatedParent as IStyleable;
@ -45,9 +45,10 @@ namespace Avalonia.Styling
return SelectorMatch.NeverThisInstance; return SelectorMatch.NeverThisInstance;
} }
return _parent.Match(templatedParent, subscribe); return _parent.Match(templatedParent, parent, subscribe);
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false;
} }
} }

3
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -94,7 +94,7 @@ namespace Avalonia.Styling
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{ {
if (TargetType != null) if (TargetType != null)
{ {
@ -140,6 +140,7 @@ namespace Avalonia.Styling
} }
protected override Selector? MovePrevious() => _previous; protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
private string BuildSelectorString() private string BuildSelectorString()
{ {

1
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -103,5 +103,6 @@
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" /> <Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" /> <PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" /> <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

3
src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs

@ -193,8 +193,11 @@ namespace Avalonia.Controls
} }
} }
/// <summary>Try get number of DataSource itmes.</summary>
/// <param name="allowSlow">When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead.</param> /// <param name="allowSlow">When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead.</param>
/// <param name="getAny">If "getAny" is true, method can use Linq.Any() method to speedup.</param> /// <param name="getAny">If "getAny" is true, method can use Linq.Any() method to speedup.</param>
/// <param name="count">number of DataSource itmes.</param>
/// <returns>true if able to retrieve number of DataSource itmes; otherwise, false.</returns>
internal bool TryGetCount(bool allowSlow, bool getAny, out int count) internal bool TryGetCount(bool allowSlow, bool getAny, out int count)
{ {
bool result; bool result;

18
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -56,6 +56,7 @@ namespace Avalonia.Controls.Platform
Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened);
Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter);
Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
_root = Menu.VisualRoot; _root = Menu.VisualRoot;
@ -91,6 +92,7 @@ namespace Avalonia.Controls.Platform
Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter);
Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
if (_root is InputElement inputRoot) 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) protected internal virtual void PointerLeave(object? sender, PointerEventArgs e)
{ {
var item = GetMenuItem(e.Source as IControl); var item = GetMenuItem(e.Source as IControl);

2
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 /// Gets or sets a collection of filters which determine the types of files displayed in an
/// <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>. /// <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary> /// </summary>
public List<FileDialogFilter> Filters { get; set; } = new List<FileDialogFilter>(); public List<FileDialogFilter>? Filters { get; set; } = new List<FileDialogFilter>();
/// <summary> /// <summary>
/// Gets or sets initial file name that is displayed when the dialog is opened. /// Gets or sets initial file name that is displayed when the dialog is opened.

4
src/Avalonia.Native/SystemDialogs.cs

@ -30,7 +30,7 @@ namespace Avalonia.Native
ofd.Title ?? "", ofd.Title ?? "",
ofd.Directory ?? "", ofd.Directory ?? "",
ofd.InitialFileName ?? "", ofd.InitialFileName ?? "",
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty<string>()));
} }
else else
{ {
@ -39,7 +39,7 @@ namespace Avalonia.Native
dialog.Title ?? "", dialog.Title ?? "",
dialog.Directory ?? "", dialog.Directory ?? "",
dialog.InitialFileName ?? "", dialog.InitialFileName ?? "",
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions))); string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty<string>()));
} }
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; });

80
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@ -7,9 +7,11 @@
</StackPanel> </StackPanel>
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
<Styles.Resources> <Styles.Resources>
<Thickness x:Key="ButtonPadding">8,5,8,6</Thickness> <Thickness x:Key="ButtonPadding">8,5,8,6</Thickness>
</Styles.Resources> </Styles.Resources>
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="Background" Value="{DynamicResource ButtonBackground}" /> <Setter Property="Background" Value="{DynamicResource ButtonBackground}" />
<!--<Setter Property="BackgroundSizing" Value="OuterBorderEdge" />--> <!--<Setter Property="BackgroundSizing" Value="OuterBorderEdge" />-->
@ -37,43 +39,54 @@
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style>
<!-- PointerOverState --> <Style.Children>
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" /> <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" /> <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" /> <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style> </Style>
<Style Selector="Button:pressed /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" /> <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" /> <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" /> <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style> </Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" /> <Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" /> <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" /> <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style> </Style>
<Style Selector="Button.accent /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="^.accent">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" /> <Style.Children>
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" /> <Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" /> <Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
</Style> <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" /> <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" /> <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" /> <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Style> </Style>
<Style Selector="Button.accent:pressed /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" /> <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" /> <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" /> <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Style.Children>
</Style>
</Style.Children>
</Style> </Style>
<Style Selector="Button, RepeatButton, ToggleButton, DropDownButton"> <Style Selector="Button, RepeatButton, ToggleButton, DropDownButton">
@ -89,9 +102,4 @@
<Setter Property="RenderTransform" Value="scale(0.98)" /> <Setter Property="RenderTransform" Value="scale(0.98)" />
</Style> </Style>
<Style Selector="Button.accent:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Styles> </Styles>

603
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -1,294 +1,321 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="CheckBox">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="20"> <Border Padding="20">
<CheckBox IsThreeState="True" IsChecked="True" Content="Content" Foreground="Gold" /> <CheckBox IsThreeState="True" IsChecked="True" Content="Content" Foreground="Gold" />
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
<Style Selector="CheckBox">
<Setter Property="Padding" Value="8,0,0,0" /> <Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" /> <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="MinHeight" Value="32" /> <Setter Property="MinHeight" Value="32" />
<!--<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" /> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" />
<Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3" />--> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" />
<Setter Property="Template"> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" />
<ControlTemplate> <Setter Property="Template">
<Grid x:Name="RootGrid" ColumnDefinitions="20,*"> <ControlTemplate>
<Border x:Name="PART_Border" <Grid x:Name="RootGrid" ColumnDefinitions="20,*">
Grid.ColumnSpan="2" <Border x:Name="PART_Border"
Background="{TemplateBinding Background}" Grid.ColumnSpan="2"
BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}" /> BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid VerticalAlignment="Top" Height="32">
<Border x:Name="NormalRectangle" <Grid VerticalAlignment="Top" Height="32">
BorderThickness="{DynamicResource CheckBoxBorderThemeThickness}" <Border x:Name="NormalRectangle"
CornerRadius="{TemplateBinding CornerRadius}" BorderThickness="{DynamicResource CheckBoxBorderThemeThickness}"
UseLayoutRounding="False" CornerRadius="{TemplateBinding CornerRadius}"
Height="20" UseLayoutRounding="False"
Width="20" /> Height="20"
Width="20" />
<Viewbox UseLayoutRounding="False">
<Panel> <Viewbox UseLayoutRounding="False">
<Panel Height="16" Width="16" /> <Panel>
<Path x:Name="CheckGlyph" Stretch="Uniform" VerticalAlignment="Center" /> <Panel Height="16" Width="16" />
</Panel> <Path x:Name="CheckGlyph" Stretch="Uniform" VerticalAlignment="Center" />
</Viewbox> </Panel>
</Grid> </Viewbox>
<ContentPresenter x:Name="ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Grid.Column="1" />
<!-- TODO: TextWrapping="Wrap" on contentpresenter -->
</Grid> </Grid>
</ControlTemplate> <ContentPresenter x:Name="ContentPresenter"
</Setter> ContentTemplate="{TemplateBinding ContentTemplate}"
</Style> Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
<!-- Unchecked Normal State --> RecognizesAccessKey="True"
<Style Selector="CheckBox"> HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" /> VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
</Style> Grid.Column="1" />
<!-- TODO: TextWrapping="Wrap" on contentpresenter -->
<Style Selector="CheckBox"> </Grid>
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" /> </ControlTemplate>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" /> </Setter>
</Style>
<Style.Children>
<Style Selector="CheckBox /template/ Border#NormalRectangle"> <!-- Unchecked Normal State -->
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}" /> <Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}" />
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}" />
</Style>
<Style Selector="CheckBox /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" /> <Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Opacity" Value="0" /> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" />
</Style> <Setter Property="Opacity" Value="0" />
</Style>
<!-- Unchecked PointerOver State -->
<Style Selector="CheckBox:pointerover /template/ ContentPresenter#ContentPresenter"> <!-- Unchecked PointerOver State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" /> <Style Selector="^:pointerover">
</Style> <Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Style Selector="CheckBox:pointerover /template/ Border#PART_Border"> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPointerOver}" /> </Style>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPointerOver}" />
</Style> <Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPointerOver}" />
<Style Selector="CheckBox:pointerover /template/ Border#NormalRectangle"> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPointerOver}" /> </Style>
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPointerOver}" />
</Style> <Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPointerOver}" />
<Style Selector="CheckBox:pointerover /template/ Path#CheckGlyph"> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPointerOver}" />
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPointerOver}" /> </Style>
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<!-- Unchecked Pressed State --> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPointerOver}" />
<Style Selector="CheckBox:pressed /template/ ContentPresenter#ContentPresenter"> </Style>
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" /> </Style.Children>
</Style> </Style>
<Style Selector="CheckBox:pressed /template/ Border#PART_Border"> <!-- Unchecked Pressed State -->
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPressed}" /> <Style Selector="^:pressed">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPressed}" /> <Style.Children>
</Style> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
<Style Selector="CheckBox:pressed /template/ Border#NormalRectangle"> </Style>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPressed}" /> <Style Selector="^ /template/ Border#PART_Border">
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPressed}" />
<Style Selector="CheckBox:pressed /template/ Path#CheckGlyph"> </Style>
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPressed}" />
</Style> <Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPressed}" />
<!-- Unchecked Disabled state --> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPressed}" />
<Style Selector="CheckBox:disabled /template/ ContentPresenter#ContentPresenter"> </Style>
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style> <Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPressed}" />
<Style Selector="CheckBox:disabled /template/ Border#PART_Border"> </Style>
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedDisabled}" /> </Style.Children>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedDisabled}" /> </Style>
</Style>
<!-- Unchecked Disabled state -->
<Style Selector="CheckBox:disabled /template/ Border#NormalRectangle"> <Style Selector="^:disabled">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedDisabled}" /> <Style.Children>
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedDisabled}" /> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
</Style> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style>
<Style Selector="CheckBox:disabled /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedDisabled}" /> <Style Selector="^ /template/ Border#PART_Border">
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedDisabled}" />
</Style>
<!-- Checked Normal State -->
<Style Selector="CheckBox:checked"> <Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundChecked}" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedDisabled}" />
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedDisabled}" />
</Style>
<Style Selector="CheckBox:checked">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundChecked}" /> <Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushChecked}" /> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedDisabled}" />
</Style> </Style>
</Style.Children>
<Style Selector="CheckBox:checked /template/ Border#NormalRectangle"> </Style>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" /> <Style Selector="^:checked">
</Style> <!-- Checked Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundChecked}" />
<Style Selector="CheckBox:checked /template/ Path#CheckGlyph"> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundChecked}" />
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundChecked}" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushChecked}" />
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" /> <Style.Children>
<Setter Property="Opacity" Value="1" /> <Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="FlowDirection" Value="LeftToRight" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
</Style>
<!-- Checked PointerOver State -->
<Style Selector="CheckBox:checked:pointerover /template/ ContentPresenter#ContentPresenter"> <Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" /> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundChecked}" />
</Style> <Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Style Selector="CheckBox:checked:pointerover /template/ Border#PART_Border"> <Setter Property="Opacity" Value="1" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPointerOver}" /> <Setter Property="FlowDirection" Value="LeftToRight" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPointerOver}" /> </Style>
</Style>
<!-- Checked PointerOver State -->
<Style Selector="CheckBox:checked:pointerover /template/ Border#NormalRectangle"> <Style Selector="^:pointerover">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPointerOver}" /> <Style.Children>
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPointerOver}" /> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
</Style> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:checked:pointerover /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPointerOver}" /> <Style Selector="^ /template/ Border#PART_Border">
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPointerOver}" />
<!-- Checked Pressed State --> </Style>
<Style Selector="CheckBox:checked:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" /> <Style Selector="^ /template/ Border#NormalRectangle">
</Style> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPointerOver}" />
<Style Selector="CheckBox:checked:pressed /template/ Border#PART_Border"> </Style>
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPressed}" /> <Style Selector="^ /template/ Path#CheckGlyph">
</Style> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:checked:pressed /template/ Border#NormalRectangle"> </Style.Children>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPressed}" /> </Style>
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPressed}" />
</Style> <!-- Checked Pressed State -->
<Style Selector="^:pressed">
<Style Selector="CheckBox:checked:pressed /template/ Path#CheckGlyph"> <Style.Children>
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPressed}" /> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
</Style> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
</Style>
<!-- Checked Disabled State -->
<Style Selector="CheckBox:checked:disabled /template/ ContentPresenter#ContentPresenter"> <Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" /> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPressed}" />
</Style> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPressed}" />
</Style>
<Style Selector="CheckBox:checked:disabled /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedDisabled}" /> <Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedDisabled}" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPressed}" />
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPressed}" />
</Style>
<Style Selector="CheckBox:checked:disabled /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedDisabled}" /> <Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedDisabled}" /> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPressed}" />
</Style> </Style>
</Style.Children>
<Style Selector="CheckBox:checked:disabled /template/ Path#CheckGlyph"> </Style>
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedDisabled}" />
</Style> <!-- Checked Disabled State -->
<Style Selector="^:disabled">
<Style.Children>
<!-- Indeterminate Normal State --> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Style Selector="CheckBox:indeterminate"> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminate}" /> </Style>
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Style Selector="CheckBox:indeterminate"> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminate}" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminate}" /> </Style>
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Style Selector="CheckBox:indeterminate /template/ Border#NormalRectangle"> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminate}" /> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminate}" /> </Style>
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Style Selector="CheckBox:indeterminate /template/ Path#CheckGlyph"> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedDisabled}" />
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminate}" /> </Style>
<Setter Property="Data" Value="M1536 1536v-1024h-1024v1024h1024z" /> </Style.Children>
<Setter Property="Width" Value="7" /> </Style>
<Setter Property="Opacity" Value="1" /> </Style.Children>
</Style> </Style>
<!-- Indeterminate PointerOver State --> <Style Selector="^:indeterminate">
<Style Selector="CheckBox:indeterminate:pointerover /template/ ContentPresenter#ContentPresenter"> <!-- Indeterminate Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" /> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminate}" />
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminate}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminate}" />
<Style Selector="CheckBox:indeterminate:pointerover /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePointerOver}" /> <Style.Children>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePointerOver}" /> <Style Selector="^ /template/ Border#NormalRectangle">
</Style> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminate}" />
<Style Selector="CheckBox:indeterminate:pointerover /template/ Border#NormalRectangle"> </Style>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePointerOver}" /> <Style Selector="^ /template/ Path#CheckGlyph">
</Style> <Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminate}" />
<Setter Property="Data" Value="M1536 1536v-1024h-1024v1024h1024z" />
<Style Selector="CheckBox:indeterminate:pointerover /template/ Path#CheckGlyph"> <Setter Property="Width" Value="7" />
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePointerOver}" /> <Setter Property="Opacity" Value="1" />
</Style> </Style>
<!-- Indeterminate Pressed State --> <!-- Indeterminate PointerOver State -->
<Style Selector="CheckBox:indeterminate:pressed /template/ ContentPresenter#ContentPresenter"> <Style Selector="^:pointerover">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" /> <Style.Children>
</Style> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
<Style Selector="CheckBox:indeterminate:pressed /template/ Border#PART_Border"> </Style>
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePressed}" /> <Style Selector="^ /template/ Border#PART_Border">
</Style> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePointerOver}" />
<Style Selector="CheckBox:indeterminate:pressed /template/ Border#NormalRectangle"> </Style>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePressed}" /> <Style Selector="^ /template/ Border#NormalRectangle">
</Style> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePointerOver}" />
<Style Selector="CheckBox:indeterminate:pressed /template/ Path#CheckGlyph"> </Style>
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePressed}" />
</Style> <Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePointerOver}" />
<!-- Indeterminate Disabled State --> </Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ ContentPresenter#ContentPresenter"> </Style.Children>
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" /> </Style>
</Style>
<!-- Indeterminate Pressed State -->
<Style Selector="CheckBox:indeterminate:disabled /template/ Border#PART_Border"> <Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminateDisabled}" /> <Style.Children>
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminateDisabled}" /> <Style Selector="^ /template/ ContentPresenter#ContentPresenter">
</Style> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
</Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminateDisabled}" /> <Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminateDisabled}" /> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePressed}" />
</Style> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePressed}" />
</Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminateDisabled}" /> <Style Selector="^ /template/ Border#NormalRectangle">
</Style> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePressed}" />
</Styles> <Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePressed}" />
</Style>
</Style.Children>
</Style>
<!-- Indeterminate Disabled State -->
<Style Selector="^:disabled">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminateDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminateDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminateDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminateDisabled}" />
</Style>
</Style.Children>
</Style>
</Style.Children>
</Style>
</Style.Children>
</Style>

24
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -151,6 +151,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
results.Add(result); results.Add(result);
result = initialNode; result = initialNode;
break; break;
case SelectorGrammar.NestingSyntax:
var parentTargetType = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>().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: default:
throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); 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")); 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<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
EmitCall(context, codeGen,
m => m.Name == "Nesting" && m.Parameters.Count == 1);
}
}
} }

30
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@ -46,7 +46,7 @@ namespace Avalonia.Markup.Parsers
switch (state) switch (state)
{ {
case State.Start: case State.Start:
state = ParseStart(ref r); (state, syntax) = ParseStart(ref r);
break; break;
case State.Middle: case State.Middle:
(state, syntax) = ParseMiddle(ref r, end); (state, syntax) = ParseMiddle(ref r, end);
@ -93,27 +93,31 @@ namespace Avalonia.Markup.Parsers
return selector; return selector;
} }
private static State ParseStart(ref CharacterReader r) private static (State, ISyntax?) ParseStart(ref CharacterReader r)
{ {
r.SkipWhitespace(); r.SkipWhitespace();
if (r.End) if (r.End)
{ {
return State.End; return (State.End, null);
} }
if (r.TakeIf(':')) if (r.TakeIf(':'))
{ {
return State.Colon; return (State.Colon, null);
} }
else if (r.TakeIf('.')) else if (r.TakeIf('.'))
{ {
return State.Class; return (State.Class, null);
} }
else if (r.TakeIf('#')) 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) private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end)
@ -142,6 +146,10 @@ namespace Avalonia.Markup.Parsers
{ {
return (State.Start, new CommaSyntax()); return (State.Start, new CommaSyntax());
} }
else if (r.TakeIf('^'))
{
return (State.CanHaveType, new NestingSyntax());
}
else if (end.HasValue && !r.End && r.Peek == end.Value) else if (end.HasValue && !r.End && r.Peek == end.Value)
{ {
return (State.End, null); return (State.End, null);
@ -635,5 +643,13 @@ namespace Avalonia.Markup.Parsers
return obj is CommaSyntax or; return obj is CommaSyntax or;
} }
} }
public class NestingSyntax : ISyntax
{
public override bool Equals(object? obj)
{
return obj is NestingSyntax;
}
}
} }
} }

6
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -103,9 +103,9 @@ namespace Avalonia.Skia
SkewY = (float)m.M12, SkewY = (float)m.M12,
ScaleY = (float)m.M22, ScaleY = (float)m.M22,
TransY = (float)m.M32, TransY = (float)m.M32,
Persp0 = 0, Persp0 = (float)m.M13,
Persp1 = 0, Persp1 = (float)m.M23,
Persp2 = 1 Persp2 = (float)m.M33
}; };
return sm; return sm;

92
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;
/// <summary>
/// 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.
/// </summary>
public class MatrixTests
{
/// <summary>
/// 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.
/// </summary>
/// <param name="expected">The expected vector</param>
/// <param name="actual">The actual transformed point</param>
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);
}
}

275
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<Control1>())
{
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<Control1>())
{
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<Panel>())
{
Children =
{
(nested = new Style(x => Selectors.Or(
x.Nesting().Child().OfType<Control1>(),
x.Nesting().Child().OfType<Control1>()))),
}
};
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<Control1>())
{
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<Control1>())
{
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<Control1>())
{
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<Control1>())
{
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<Panel>())
{
Children =
{
(nested = new Style(x => Selectors.Or(
x.Nesting().Child().OfType<Control1>(),
x.Nesting().Child().OfType<Control1>()))),
}
};
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<Control1>());
Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => nested.Selector.Match(control, parent));
}
[Fact]
public void Adding_Child_With_No_Nesting_Selector_Fails()
{
var parent = new Style(x => x.OfType<Control1>());
var child = new Style(x => x.Class("foo"));
Assert.Throws<InvalidOperationException>(() => parent.Children.Add(child));
}
[Fact]
public void Adding_Combinator_Selector_Child_With_No_Nesting_Selector_Fails()
{
var parent = new Style(x => x.OfType<Control1>());
var child = new Style(x => x.Class("foo").Descendant().Class("bar"));
Assert.Throws<InvalidOperationException>(() => parent.Children.Add(child));
}
[Fact]
public void Adding_Or_Selector_Child_With_No_Nesting_Selector_Fails()
{
var parent = new Style(x => x.OfType<Control1>());
var child = new Style(x => Selectors.Or(
x.Nesting().Class("foo"),
x.Class("bar")));
Assert.Throws<InvalidOperationException>(() => 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;
}
}
}

42
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); resources.Verify(x => x.AddOwner(host.Object), Times.Once);
} }
[Fact]
public void Nested_Style_Can_Be_Added()
{
var parent = new Style(x => x.OfType<Class1>());
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<Class1>());
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<Class1>());
var nested = new Style();
Assert.Throws<InvalidOperationException>(() => parent.Children.Add(nested));
}
[Fact(Skip = "TODO")]
public void Nested_Style_Without_Nesting_Operator_Throws()
{
var parent = new Style(x => x.OfType<Class1>());
var nested = new Style(x => x.Class("foo"));
Assert.Throws<InvalidOperationException>(() => parent.Children.Add(nested));
}
private class Class1 : Control private class Class1 : Control
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

138
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -469,6 +469,144 @@ namespace Avalonia.Markup.UnitTests.Parsers
result); 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] [Fact]
public void Namespace_Alone_Fails() public void Namespace_Alone_Fails()
{ {

32
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); Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color);
} }
} }
[Fact]
public void Can_Use_Nested_Styles()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border'>
<Style.Children>
<Style Selector='^.foo'>
<Setter Property='Background' Value='Red'/>
</Style>
</Style.Children>
</Style>
</Window.Styles>
<StackPanel>
<Border Name='foo'/>
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var foo = window.FindControl<Border>("foo");
Assert.Null(foo.Background);
foo.Classes.Add("foo");
Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color);
}
}
} }
} }

Loading…
Cancel
Save