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">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.254" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2-preview.254" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" 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" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2" />
</ItemGroup>
</Project>

6
build/SkiaSharp.props

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

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

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

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

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

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

@ -16,8 +16,6 @@
class WindowBaseImpl : public virtual ComObject,
public virtual IAvnWindowBase,
public INSWindowHolder {
private:
NSCursor *cursor;
public:
FORWARD_IUNKNOWN()
@ -28,23 +26,6 @@ BEGIN_INTERFACE_MAP()
virtual ~WindowBaseImpl();
AutoFitContentView *StandardContainer;
AvnView *View;
NSWindow * Window;
ComPtr<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);
virtual HRESULT ObtainNSWindowHandle(void **ret) override;
@ -123,11 +104,33 @@ protected:
virtual NSWindowStyleMask GetStyle();
void UpdateStyle();
virtual void OnInitialiseNSWindow ();
private:
void CreateNSWindow (bool isDialog);
void CleanNSWindow ();
void InitialiseNSWindow ();
NSCursor *cursor;
ComPtr<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

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

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

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

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

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

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

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

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

3
samples/ControlCatalog/ControlCatalog.csproj

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

1
samples/ControlCatalog/Pages/CarouselPage.xaml

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

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

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

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

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

3
samples/RenderDemo/MainWindow.xaml

@ -72,5 +72,8 @@
<TabItem Header="Brushes">
<pages:BrushesPage />
</TabItem>
<TabItem Header="3D Transformation">
<pages:Transform3DPage />
</TabItem>
</controls:HamburgerMenu>
</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);
_framebuffer = new FramebufferManager(this);
RenderScaling = (int)_view.Scaling;
RenderScaling = _view.Scaling;
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_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 RotateTransform());
normalTransform.Children.Add(new TranslateTransform());
normalTransform.Children.Add(new Rotate3DTransform());
ctrl.RenderTransform = normalTransform;
}

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

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

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>
/// <remarks>
/// 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
/// 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.

268
src/Avalonia.Base/Matrix.cs

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

21
src/Avalonia.Base/Point.cs

@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Numerics;
#if !BUILDTASK
using Avalonia.Animation.Animators;
#endif
@ -168,12 +169,7 @@ namespace Avalonia
/// <param name="point">The point.</param>
/// <param name="matrix">The matrix.</param>
/// <returns>The resulting point.</returns>
public static Point operator *(Point point, Matrix matrix)
{
return new Point(
(point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31,
(point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32);
}
public static Point operator *(Point point, Matrix matrix) => matrix.Transform(point);
/// <summary>
/// Parses a <see cref="Point"/> string.
@ -242,18 +238,7 @@ namespace Avalonia
/// </summary>
/// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns>
public Point Transform(Matrix transform)
{
var x = X;
var y = Y;
var xadd = y * transform.M21 + transform.M31;
var yadd = x * transform.M12 + transform.M32;
x *= transform.M11;
x += xadd;
y *= transform.M22;
y += yadd;
return new Point(x, y);
}
public Point Transform(Matrix transform) => transform.Transform(this);
/// <summary>
/// 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;
}
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
var controlParent = ((ILogical)control).LogicalParent;
if (controlParent != null)
{
var parentMatch = _parent.Match((IStyleable)controlParent, subscribe);
var parentMatch = _parent.Match((IStyleable)controlParent, parent, subscribe);
if (parentMatch.Result == SelectorMatchResult.Sometimes)
{
@ -65,5 +65,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
}
}

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

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

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;
}
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
var innerResult = _argument.Match(control, subscribe);
var innerResult = _argument.Match(control, parent, subscribe);
switch (innerResult.Result)
{
@ -67,5 +67,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector();
}
}

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

@ -48,7 +48,7 @@ namespace Avalonia.Styling
public int Step { get; }
public int Offset { get; }
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (!(control is ILogical logical))
{
@ -105,6 +105,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
public override string ToString()
{

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

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

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

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

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

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

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>
/// Returns a selector which inverts the results of selector argument.
/// </summary>

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

@ -4,8 +4,6 @@ using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -14,9 +12,11 @@ namespace Avalonia.Styling
public class Style : AvaloniaObject, IStyle, IResourceProvider
{
private IResourceHost? _owner;
private StyleChildren? _children;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
private StyleCache? _childCache;
/// <summary>
/// Initializes a new instance of the <see cref="Style"/> class.
@ -34,6 +34,14 @@ namespace Avalonia.Styling
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
{
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>
/// Gets or sets a dictionary of style resources.
/// </summary>
@ -90,7 +103,7 @@ namespace Avalonia.Styling
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
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;
@ -98,7 +111,7 @@ namespace Avalonia.Styling
{
target = target ?? throw new ArgumentNullException(nameof(target));
var match = Selector is object ? Selector.Match(target) :
var match = Selector is object ? Selector.Match(target, Parent) :
target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
if (match.IsMatch && (_setters is object || _animations is object))
@ -108,7 +121,17 @@ namespace Avalonia.Styling
instance.Start();
}
return match.Result;
var result = match.Result;
if (_children is not null)
{
_childCache ??= new StyleCache();
var childResult = _childCache.TryAttach(_children, target, host);
if (childResult > result)
result = childResult;
}
return result;
}
public bool TryGetResource(object key, out object? result)
@ -156,5 +179,18 @@ namespace Avalonia.Styling
_resources?.RemoveOwner(owner);
}
}
internal void SetParent(Style? parent)
{
if (parent?.Selector is not null)
{
if (Selector is null)
throw new InvalidOperationException("Child styles must have a selector.");
if (!Selector.HasValidNestingSelector())
throw new InvalidOperationException("Child styles must have a nesting selector.");
}
Parent = parent;
}
}
}

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 IResourceHost? _owner;
private IResourceDictionary? _resources;
private Dictionary<Type, List<IStyle>?>? _cache;
private StyleCache? _cache;
public Styles()
{
@ -111,43 +111,8 @@ namespace Avalonia.Styling
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{
_cache ??= new Dictionary<Type, List<IStyle>?>();
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;
}
_cache ??= new StyleCache();
return _cache.TryAttach(this, target, host);
}
/// <inheritdoc/>

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

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

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

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

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" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
</ItemGroup>
</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="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)
{
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(MenuItem.PointerEnterItemEvent, PointerEnter);
Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave);
Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
_root = Menu.VisualRoot;
@ -91,6 +92,7 @@ namespace Avalonia.Controls.Platform
Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened);
Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter);
Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave);
Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
if (_root is InputElement inputRoot)
{
@ -340,6 +342,22 @@ namespace Avalonia.Controls.Platform
}
}
protected internal virtual void PointerMoved(object? sender, PointerEventArgs e)
{
// HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method.
var item = GetMenuItem(e.Source as IControl) as MenuItem;
if (item?.TransformedBounds == null)
{
return;
}
var point = e.GetCurrentPoint(null);
if (point.Properties.IsLeftButtonPressed && item.TransformedBounds.Value.Contains(point.Position) == false)
{
e.Pointer.Capture(null);
}
}
protected internal virtual void PointerLeave(object? sender, PointerEventArgs e)
{
var item = GetMenuItem(e.Source as IControl);

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
/// <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
public List<FileDialogFilter> Filters { get; set; } = new List<FileDialogFilter>();
public List<FileDialogFilter>? Filters { get; set; } = new List<FileDialogFilter>();
/// <summary>
/// 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.Directory ?? "",
ofd.InitialFileName ?? "",
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions)));
string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty<string>()));
}
else
{
@ -39,7 +39,7 @@ namespace Avalonia.Native
dialog.Title ?? "",
dialog.Directory ?? "",
dialog.InitialFileName ?? "",
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions)));
string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty<string>()));
}
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; });

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

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

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>
<Border Padding="20">
<CheckBox IsThreeState="True" IsChecked="True" Content="Content" Foreground="Gold" />
</Border>
</Design.PreviewWith>
<Style Selector="CheckBox">
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="MinHeight" Value="32" />
<!--<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,-3,-7,-3" />-->
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="RootGrid" ColumnDefinitions="20,*">
<Border x:Name="PART_Border"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid VerticalAlignment="Top" Height="32">
<Border x:Name="NormalRectangle"
BorderThickness="{DynamicResource CheckBoxBorderThemeThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
UseLayoutRounding="False"
Height="20"
Width="20" />
<Viewbox UseLayoutRounding="False">
<Panel>
<Panel Height="16" Width="16" />
<Path x:Name="CheckGlyph" Stretch="Uniform" VerticalAlignment="Center" />
</Panel>
</Viewbox>
</Grid>
<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 -->
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="RootGrid" ColumnDefinitions="20,*">
<Border x:Name="PART_Border"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid VerticalAlignment="Top" Height="32">
<Border x:Name="NormalRectangle"
BorderThickness="{DynamicResource CheckBoxBorderThemeThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
UseLayoutRounding="False"
Height="20"
Width="20" />
<Viewbox UseLayoutRounding="False">
<Panel>
<Panel Height="16" Width="16" />
<Path x:Name="CheckGlyph" Stretch="Uniform" VerticalAlignment="Center" />
</Panel>
</Viewbox>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- Unchecked Normal State -->
<Style Selector="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" />
</Style>
<Style Selector="CheckBox">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" />
</Style>
<Style Selector="CheckBox /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}" />
</Style>
<Style Selector="CheckBox /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" />
<Setter Property="Opacity" Value="0" />
</Style>
<!-- Unchecked PointerOver State -->
<Style Selector="CheckBox:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:pointerover /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:pointerover /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:pointerover /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPointerOver}" />
</Style>
<!-- Unchecked Pressed State -->
<Style Selector="CheckBox:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
</Style>
<Style Selector="CheckBox:pressed /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPressed}" />
</Style>
<Style Selector="CheckBox:pressed /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPressed}" />
</Style>
<Style Selector="CheckBox:pressed /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPressed}" />
</Style>
<!-- Unchecked Disabled state -->
<Style Selector="CheckBox:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style>
<Style Selector="CheckBox:disabled /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedDisabled}" />
</Style>
<Style Selector="CheckBox:disabled /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedDisabled}" />
</Style>
<Style Selector="CheckBox:disabled /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedDisabled}" />
</Style>
<!-- Checked Normal State -->
<Style Selector="CheckBox:checked">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundChecked}" />
</Style>
<Style Selector="CheckBox:checked">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundChecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushChecked}" />
</Style>
<Style Selector="CheckBox:checked /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
</Style>
<Style Selector="CheckBox:checked /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundChecked}" />
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
<!-- Checked PointerOver State -->
<Style Selector="CheckBox:checked:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:checked:pointerover /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:checked:pointerover /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:checked:pointerover /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPointerOver}" />
</Style>
<!-- Checked Pressed State -->
<Style Selector="CheckBox:checked:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
</Style>
<Style Selector="CheckBox:checked:pressed /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPressed}" />
</Style>
<Style Selector="CheckBox:checked:pressed /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPressed}" />
</Style>
<Style Selector="CheckBox:checked:pressed /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPressed}" />
</Style>
<!-- Checked Disabled State -->
<Style Selector="CheckBox:checked:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
</Style>
<Style Selector="CheckBox:checked:disabled /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedDisabled}" />
</Style>
<Style Selector="CheckBox:checked:disabled /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedDisabled}" />
</Style>
<Style Selector="CheckBox:checked:disabled /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedDisabled}" />
</Style>
<!-- Indeterminate Normal State -->
<Style Selector="CheckBox:indeterminate">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminate}" />
</Style>
<Style Selector="CheckBox:indeterminate">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminate}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminate}" />
</Style>
<Style Selector="CheckBox:indeterminate /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminate}" />
</Style>
<Style Selector="CheckBox:indeterminate /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminate}" />
<Setter Property="Data" Value="M1536 1536v-1024h-1024v1024h1024z" />
<Setter Property="Width" Value="7" />
<Setter Property="Opacity" Value="1" />
</Style>
<!-- Indeterminate PointerOver State -->
<Style Selector="CheckBox:indeterminate:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
</Style>
<Style Selector="CheckBox:indeterminate:pointerover /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePointerOver}" />
</Style>
<Style Selector="CheckBox:indeterminate:pointerover /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePointerOver}" />
</Style>
<Style Selector="CheckBox:indeterminate:pointerover /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePointerOver}" />
</Style>
<!-- Indeterminate Pressed State -->
<Style Selector="CheckBox:indeterminate:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
</Style>
<Style Selector="CheckBox:indeterminate:pressed /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePressed}" />
</Style>
<Style Selector="CheckBox:indeterminate:pressed /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePressed}" />
</Style>
<Style Selector="CheckBox:indeterminate:pressed /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePressed}" />
</Style>
<!-- Indeterminate Disabled State -->
<Style Selector="CheckBox:indeterminate:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
</Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminateDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminateDisabled}" />
</Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminateDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminateDisabled}" />
</Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminateDisabled}" />
</Style>
</Styles>
<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>
</ControlTemplate>
</Setter>
<Style.Children>
<!-- Unchecked Normal State -->
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUnchecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUnchecked}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" />
<Setter Property="Opacity" Value="0" />
</Style>
<!-- Unchecked PointerOver State -->
<Style Selector="^:pointerover">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPointerOver}" />
</Style>
</Style.Children>
</Style>
<!-- Unchecked Pressed State -->
<Style Selector="^:pressed">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedPressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedPressed}" />
</Style>
</Style.Children>
</Style>
<!-- Unchecked Disabled state -->
<Style Selector="^:disabled">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUncheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeUncheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillUncheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundUncheckedDisabled}" />
</Style>
</Style.Children>
</Style>
<Style Selector="^:checked">
<!-- Checked Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundChecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushChecked}" />
<Style.Children>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillChecked}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundChecked}" />
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
<!-- Checked PointerOver State -->
<Style Selector="^:pointerover">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPointerOver}" />
</Style>
</Style.Children>
</Style>
<!-- Checked Pressed State -->
<Style Selector="^:pressed">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedPressed}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedPressed}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedPressed}" />
</Style>
</Style.Children>
</Style>
<!-- Checked Disabled State -->
<Style Selector="^:disabled">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundCheckedDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeCheckedDisabled}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillCheckedDisabled}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundCheckedDisabled}" />
</Style>
</Style.Children>
</Style>
</Style.Children>
</Style>
<Style Selector="^:indeterminate">
<!-- Indeterminate Normal State -->
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminate}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminate}" />
<Style.Children>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminate}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminate}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminate}" />
<Setter Property="Data" Value="M1536 1536v-1024h-1024v1024h1024z" />
<Setter Property="Width" Value="7" />
<Setter Property="Opacity" Value="1" />
</Style>
<!-- Indeterminate PointerOver State -->
<Style Selector="^:pointerover">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePointerOver}" />
<Setter Property="Background" Value="{DynamicResource CheckBoxCheckBackgroundFillIndeterminatePointerOver}" />
</Style>
<Style Selector="^ /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{DynamicResource CheckBoxCheckGlyphForegroundIndeterminatePointerOver}" />
</Style>
</Style.Children>
</Style>
<!-- Indeterminate Pressed State -->
<Style Selector="^:pressed">
<Style.Children>
<Style Selector="^ /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Border#PART_Border">
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundIndeterminatePressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushIndeterminatePressed}" />
</Style>
<Style Selector="^ /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxCheckBackgroundStrokeIndeterminatePressed}" />
<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);
result = initialNode;
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:
throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node);
}
@ -474,4 +482,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList"));
}
}
class XamlIlNestingSelector : XamlIlSelectorNode
{
public XamlIlNestingSelector(XamlIlSelectorNode previous, IXamlType targetType)
: base(previous)
{
TargetType = targetType;
}
public override IXamlType TargetType { get; }
protected override void DoEmit(XamlEmitContext<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)
{
case State.Start:
state = ParseStart(ref r);
(state, syntax) = ParseStart(ref r);
break;
case State.Middle:
(state, syntax) = ParseMiddle(ref r, end);
@ -93,27 +93,31 @@ namespace Avalonia.Markup.Parsers
return selector;
}
private static State ParseStart(ref CharacterReader r)
private static (State, ISyntax?) ParseStart(ref CharacterReader r)
{
r.SkipWhitespace();
if (r.End)
{
return State.End;
return (State.End, null);
}
if (r.TakeIf(':'))
{
return State.Colon;
return (State.Colon, null);
}
else if (r.TakeIf('.'))
{
return State.Class;
return (State.Class, null);
}
else if (r.TakeIf('#'))
{
return State.Name;
return (State.Name, null);
}
else if (r.TakeIf('^'))
{
return (State.CanHaveType, new NestingSyntax());
}
return State.TypeName;
return (State.TypeName, null);
}
private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end)
@ -142,6 +146,10 @@ namespace Avalonia.Markup.Parsers
{
return (State.Start, new CommaSyntax());
}
else if (r.TakeIf('^'))
{
return (State.CanHaveType, new NestingSyntax());
}
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
return (State.End, null);
@ -635,5 +643,13 @@ namespace Avalonia.Markup.Parsers
return obj is CommaSyntax or;
}
}
public class NestingSyntax : ISyntax
{
public override bool Equals(object? obj)
{
return obj is NestingSyntax;
}
}
}
}

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

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

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);
}
[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
{
public static readonly StyledProperty<string> FooProperty =

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

@ -469,6 +469,144 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Fact]
public void Nesting_Class()
{
var result = SelectorGrammar.Parse("^.foo");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
result);
}
[Fact]
public void Nesting_Child_Class()
{
var result = SelectorGrammar.Parse("^ > .foo");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.ChildSyntax { },
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
result);
}
[Fact]
public void Nesting_Descendant_Class()
{
var result = SelectorGrammar.Parse("^ .foo");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.DescendantSyntax { },
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
result);
}
[Fact]
public void Nesting_Template_Class()
{
var result = SelectorGrammar.Parse("^ /template/ .foo");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.TemplateSyntax { },
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
result);
}
[Fact]
public void OfType_Template_Nesting()
{
var result = SelectorGrammar.Parse("Button /template/ ^");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.TemplateSyntax { },
new SelectorGrammar.NestingSyntax(),
},
result);
}
[Fact]
public void Nesting_Property()
{
var result = SelectorGrammar.Parse("^[Foo=bar]");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.PropertySyntax { Property = "Foo", Value = "bar" },
},
result);
}
[Fact]
public void Not_Nesting()
{
var result = SelectorGrammar.Parse(":not(^)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NotSyntax
{
Argument = new[] { new SelectorGrammar.NestingSyntax() },
}
},
result);
}
[Fact]
public void Nesting_NthChild()
{
var result = SelectorGrammar.Parse("^:nth-child(2n+1)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void Nesting_Comma_Nesting_Class()
{
var result = SelectorGrammar.Parse("^, ^.foo");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.CommaSyntax(),
new SelectorGrammar.NestingSyntax(),
new SelectorGrammar.ClassSyntax { Class = "foo" },
},
result);
}
[Fact]
public void Namespace_Alone_Fails()
{

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