Browse Source

Merge branch 'feature/extend-client-area-to-decorations' into feature/test-branch

# Conflicts:
#	samples/ControlCatalog/MainWindow.xaml
feature/test-branch
Dan Walmsley 6 years ago
parent
commit
f42829abcc
  1. 14
      native/Avalonia.Native/inc/avalonia-native.h
  2. 13
      native/Avalonia.Native/src/OSX/window.h
  3. 284
      native/Avalonia.Native/src/OSX/window.mm
  4. 1
      samples/ControlCatalog/MainView.xaml
  5. 7
      samples/ControlCatalog/MainView.xaml.cs
  6. 42
      samples/ControlCatalog/MainWindow.xaml
  7. 20
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  8. 19
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs
  9. 88
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  10. 31
      src/Avalonia.Base/Utilities/MathUtilities.cs
  11. 63
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  12. 97
      src/Avalonia.Controls/Chrome/TitleBar.cs
  13. 6
      src/Avalonia.Controls/LayoutTransformControl.cs
  14. 21
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  15. 14
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  16. 37
      src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
  17. 15
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  18. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  19. 2
      src/Avalonia.Controls/TopLevel.cs
  20. 130
      src/Avalonia.Controls/Window.cs
  21. 22
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  22. 22
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  23. 85
      src/Avalonia.Native/WindowImpl.cs
  24. 14
      src/Avalonia.Native/WindowImplBase.cs
  25. 67
      src/Avalonia.Themes.Default/CaptionButtons.xaml
  26. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  27. 19
      src/Avalonia.Themes.Default/TitleBar.xaml
  28. 4
      src/Avalonia.Themes.Default/Window.xaml
  29. 70
      src/Avalonia.Themes.Fluent/CaptionButtons.xaml
  30. 4
      src/Avalonia.Themes.Fluent/FluentTheme.xaml
  31. 53
      src/Avalonia.Themes.Fluent/TitleBar.xaml
  32. 10
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  33. 35
      src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs
  34. 25
      src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
  35. 72
      src/Avalonia.Visuals/Matrix.cs
  36. 12
      src/Avalonia.Visuals/Media/IMutableTransform.cs
  37. 10
      src/Avalonia.Visuals/Media/ITransform.cs
  38. 5
      src/Avalonia.Visuals/Media/Transform.cs
  39. 23
      src/Avalonia.Visuals/Media/TransformConverter.cs
  40. 40
      src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs
  41. 203
      src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs
  42. 252
      src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs
  43. 463
      src/Avalonia.Visuals/Media/Transformation/TransformParser.cs
  44. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  45. 14
      src/Avalonia.Visuals/Visual.cs
  46. 2
      src/Avalonia.Visuals/VisualTree/IVisual.cs
  47. 22
      src/Avalonia.X11/X11Window.cs
  48. 9
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  49. 526
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  50. 135
      src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
  51. 500
      src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
  52. 160
      src/Windows/Avalonia.Win32/WindowImpl.cs
  53. 16
      tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs
  54. 89
      tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
  55. 133
      tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs

14
native/Avalonia.Native/inc/avalonia-native.h

@ -204,6 +204,16 @@ enum AvnMenuItemToggleType
Radio
};
enum AvnExtendClientAreaChromeHints
{
AvnChromeHintsNoChrome,
AvnChromeHintsSystemTitleBar = 0x01,
AvnChromeHintsManagedChromeButtons = 0x02,
AvnChromeHintsSystemChromeButtons = 0x04,
AvnChromeHintsOSXThickTitleBar = 0x08,
AvnChromeHintsDefault = AvnChromeHintsSystemTitleBar | AvnChromeHintsSystemChromeButtons,
};
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{
public:
@ -276,6 +286,10 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0;
virtual HRESULT GetWindowState(AvnWindowState*ret) = 0;
virtual HRESULT SetExtendClientArea (bool enable) = 0;
virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) = 0;
virtual HRESULT GetExtendTitleBarHeight (double*ret) = 0;
virtual HRESULT SetExtendTitleBarHeight (double value) = 0;
};
AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown

13
native/Avalonia.Native/src/OSX/window.h

@ -3,9 +3,6 @@
class WindowBaseImpl;
@interface AutoFitContentVisualEffectView : NSVisualEffectView
@end
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
@ -15,6 +12,14 @@ class WindowBaseImpl;
-(AvnPixelSize) getPixelSize;
@end
@interface AutoFitContentView : NSView
-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content;
-(void) ShowTitleBar: (bool) show;
-(void) SetTitleBarHeightHint: (double) height;
-(void) SetContent: (NSView* _Nonnull) content;
-(void) ShowBlur: (bool) show;
@end
@interface AvnWindow : NSWindow <NSWindowDelegate>
+(void) closeAll;
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
@ -27,6 +32,8 @@ class WindowBaseImpl;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu;
-(double) getScaling;
-(double) getExtendedTitleBarHeight;
-(void) setIsExtended:(bool)value;
@end
struct INSWindowHolder

284
native/Avalonia.Native/src/OSX/window.mm

@ -6,8 +6,6 @@
#include <OpenGL/gl.h>
#include "rendertarget.h"
class WindowBaseImpl : public virtual ComSingleObject<IAvnWindowBase, &IID_IAvnWindowBase>, public INSWindowHolder
{
private:
@ -20,7 +18,7 @@ public:
View = NULL;
Window = NULL;
}
NSVisualEffectView* VisualEffect;
AutoFitContentView* StandardContainer;
AvnView* View;
AvnWindow* Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents;
@ -39,6 +37,7 @@ public:
_glContext = gl;
renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl];
View = [[AvnView alloc] initWithParent:this];
StandardContainer = [[AutoFitContentView new] initWithContent:View];
Window = [[AvnWindow alloc] initWithParent:this];
@ -49,12 +48,8 @@ public:
[Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered];
VisualEffect = [AutoFitContentVisualEffectView new];
[VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[VisualEffect setMaterial:NSVisualEffectMaterialLight];
[VisualEffect setAutoresizesSubviews:true];
[Window setContentView: View];
[Window setOpaque:false];
[Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -392,12 +387,7 @@ public:
virtual HRESULT SetBlurEnabled (bool enable) override
{
[Window setContentView: enable ? VisualEffect : View];
if(enable)
{
[VisualEffect addSubview:View];
}
[StandardContainer ShowBlur:enable];
return S_OK;
}
@ -474,6 +464,8 @@ private:
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
bool _isClientAreaExtended;
AvnExtendClientAreaChromeHints _extendClientHints;
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
@ -487,6 +479,8 @@ private:
ComPtr<IAvnWindowEvents> WindowEvents;
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
{
_isClientAreaExtended = false;
_extendClientHints = AvnChromeHintsDefault;
_fullScreenActive = false;
_canResize = true;
_decorations = SystemDecorationsFull;
@ -505,8 +499,18 @@ private:
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
if ([button isKindOfClass:[NSButton class]]) {
[button setHidden: (_decorations != SystemDecorationsFull)];
if ([button isKindOfClass:[NSButton class]])
{
if(_isClientAreaExtended)
{
[button setHidden: !(_extendClientHints & AvnChromeHintsSystemChromeButtons)];
}
else
{
[button setHidden: (_decorations != SystemDecorationsFull)];
}
[button setWantsLayer:true];
}
}
}
@ -582,6 +586,35 @@ private:
if(_lastWindowState != state)
{
if(_isClientAreaExtended)
{
if(_lastWindowState == FullScreen)
{
// we exited fs.
if(_extendClientHints & AvnChromeHintsOSXThickTitleBar)
{
Window.toolbar = [NSToolbar new];
Window.toolbar.showsBaselineSeparator = false;
}
[Window setTitlebarAppearsTransparent:true];
[StandardContainer setFrameSize: StandardContainer.frame.size];
}
else if(state == FullScreen)
{
// we entered fs.
if(_extendClientHints & AvnChromeHintsOSXThickTitleBar)
{
Window.toolbar = nullptr;
}
[Window setTitlebarAppearsTransparent:false];
[StandardContainer setFrameSize: StandardContainer.frame.size];
}
}
_lastWindowState = state;
WindowEvents->WindowStateChanged(state);
}
@ -638,8 +671,6 @@ private:
return S_OK;
}
auto currentFrame = [Window frame];
UpdateStyle();
HideOrShowTrafficLights();
@ -766,6 +797,78 @@ private:
}
}
virtual HRESULT SetExtendClientArea (bool enable) override
{
_isClientAreaExtended = enable;
if(enable)
{
Window.titleVisibility = NSWindowTitleHidden;
[Window setTitlebarAppearsTransparent:true];
if(_extendClientHints & AvnChromeHintsSystemTitleBar)
{
[StandardContainer ShowTitleBar:true];
}
else
{
[StandardContainer ShowTitleBar:false];
}
if(_extendClientHints & AvnChromeHintsOSXThickTitleBar)
{
Window.toolbar = [NSToolbar new];
Window.toolbar.showsBaselineSeparator = false;
}
else
{
Window.toolbar = nullptr;
}
}
else
{
Window.titleVisibility = NSWindowTitleVisible;
Window.toolbar = nullptr;
[Window setTitlebarAppearsTransparent:false];
View.layer.zPosition = 0;
}
[Window setIsExtended:enable];
HideOrShowTrafficLights();
UpdateStyle();
return S_OK;
}
virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override
{
_extendClientHints = hints;
SetExtendClientArea(_isClientAreaExtended);
return S_OK;
}
virtual HRESULT GetExtendTitleBarHeight (double*ret) override
{
if(ret == nullptr)
{
return E_POINTER;
}
*ret = [Window getExtendedTitleBarHeight];
return S_OK;
}
virtual HRESULT SetExtendTitleBarHeight (double value) override
{
[StandardContainer SetTitleBarHeightHint:value];
return S_OK;
}
void EnterFullScreenMode ()
{
_fullScreenActive = true;
@ -775,8 +878,9 @@ private:
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
[Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable];
Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable;
Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView;
[Window toggleFullScreen:nullptr];
}
@ -924,19 +1028,120 @@ protected:
{
s |= NSWindowStyleMaskMiniaturizable;
}
if(_isClientAreaExtended)
{
s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground;
}
return s;
}
};
NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil];
@implementation AutoFitContentVisualEffectView
@implementation AutoFitContentView
{
NSVisualEffectView* _titleBarMaterial;
NSBox* _titleBarUnderline;
NSView* _content;
NSVisualEffectView* _blurBehind;
double _titleBarHeightHint;
bool _settingSize;
}
-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content
{
_titleBarHeightHint = -1;
_content = content;
_settingSize = false;
[self setAutoresizesSubviews:true];
[self setWantsLayer:true];
_titleBarMaterial = [NSVisualEffectView new];
[_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow];
[_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar];
[_titleBarMaterial setWantsLayer:true];
_titleBarMaterial.hidden = true;
_titleBarUnderline = [NSBox new];
_titleBarUnderline.boxType = NSBoxSeparator;
_titleBarUnderline.fillColor = [NSColor underPageBackgroundColor];
_titleBarUnderline.hidden = true;
[self addSubview:_titleBarMaterial];
[self addSubview:_titleBarUnderline];
_blurBehind = [NSVisualEffectView new];
[_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[_blurBehind setMaterial:NSVisualEffectMaterialLight];
[_blurBehind setWantsLayer:true];
_blurBehind.hidden = true;
[self addSubview:_blurBehind];
[self addSubview:_content];
[self setWantsLayer:true];
return self;
}
-(void) ShowBlur:(bool)show
{
_blurBehind.hidden = !show;
}
-(void) ShowTitleBar: (bool) show
{
_titleBarMaterial.hidden = !show;
_titleBarUnderline.hidden = !show;
}
-(void) SetTitleBarHeightHint: (double) height
{
_titleBarHeightHint = height;
[self setFrameSize:self.frame.size];
}
-(void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
if([[self subviews] count] == 0)
if(_settingSize)
{
return;
[[self subviews][0] setFrameSize: newSize];
}
_settingSize = true;
[super setFrameSize:newSize];
[_blurBehind setFrameSize:newSize];
[_content setFrameSize:newSize];
auto window = objc_cast<AvnWindow>([self window]);
// TODO get actual titlebar size
double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint;
NSRect tbar;
tbar.origin.x = 0;
tbar.origin.y = newSize.height - height;
tbar.size.width = newSize.width;
tbar.size.height = height;
[_titleBarMaterial setFrame:tbar];
tbar.size.height = height < 1 ? 0 : 1;
[_titleBarUnderline setFrame:tbar];
_settingSize = false;
}
-(void) SetContent: (NSView* _Nonnull) content
{
if(content != nullptr)
{
[content removeFromSuperview];
[self addSubview:content];
_content = content;
}
}
@end
@ -1467,15 +1672,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _canBecomeKeyAndMain;
bool _closed;
bool _isEnabled;
bool _isExtended;
AvnMenu* _menu;
double _lastScaling;
}
-(void) setIsExtended:(bool)value;
{
_isExtended = value;
}
-(double) getScaling
{
return _lastScaling;
}
-(double) getExtendedTitleBarHeight
{
if(_isExtended)
{
for (id subview in self.contentView.superview.subviews)
{
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")])
{
NSView *titlebarView = [subview subviews][0];
return (double)titlebarView.frame.size.height;
}
}
return -1;
}
else
{
return 0;
}
}
+(void)closeAll
{
NSArray<NSWindow*>* windows = [NSArray arrayWithArray:[NSApp windows]];
@ -1594,6 +1827,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]];
[self invalidateShadow];
_isExtended = false;
return self;
}

1
samples/ControlCatalog/MainView.xaml

@ -60,6 +60,7 @@
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabItem Header="Window Customizations"><pages:WindowCustomizationsPage/></TabItem>
<TabControl.Tag>
<StackPanel Width="115" Spacing="4" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="8">
<ComboBox x:Name="Decorations" SelectedIndex="0">

7
samples/ControlCatalog/MainView.xaml.cs

@ -58,13 +58,6 @@ namespace ControlCatalog
if (VisualRoot is Window window)
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
};
var transparencyLevels = this.Find<ComboBox>("TransparencyLevels");
transparencyLevels.SelectionChanged += (sender, e) =>
{
if (VisualRoot is Window window)
window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex;
};
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)

42
samples/ControlCatalog/MainWindow.xaml

@ -7,8 +7,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views"
TransparencyLevelHint="AcrylicBlur"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent">
ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
ExtendClientAreaChromeHints="{Binding ChromeHints}"
ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
TransparencyLevelHint="{Binding TransparencyLevel}"
x:Name="MainWindow"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{x:Null}">
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="File">
@ -57,20 +61,30 @@
</NativeMenu>
</NativeMenu.Menu>
<Window.DataTemplates>
<Window.DataTemplates>
<DataTemplate DataType="vm:NotificationViewModel">
<v:CustomNotificationView />
</DataTemplate>
</Window.DataTemplates>
<DockPanel LastChildFill="True">
<Menu Name="MainMenu" DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Exit" Command="{Binding ExitCommand}" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Header="About" Command="{Binding AboutCommand}" />
</MenuItem>
</Menu>
<local:MainView />
</DockPanel>
<Panel>
<Panel Margin="{Binding #MainWindow.OffScreenMargin}">
<DockPanel LastChildFill="True" Margin="{Binding #MainWindow.WindowDecorationMargins}">
<Menu Name="MainMenu" DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Exit" Command="{Binding ExitCommand}" />
</MenuItem>
<MenuItem Header="Help">
<MenuItem Header="About" Command="{Binding AboutCommand}" />
</MenuItem>
</Menu>
<local:MainView />
</DockPanel>
</Panel>
<Border IsVisible="{Binding ExtendClientAreaEnabled}" BorderThickness="1 1 1 0" CornerRadius="4 4 0 0" BorderBrush="#55000000" Height="22" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="250 8 0 0">
<Border.Background>
<SolidColorBrush Color="White" Opacity="0.7" />
</Border.Background>
<TextBlock Margin="5 5 5 0" Text="Content In TitleBar" />
</Border>
</Panel>
</Window>

20
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -0,0 +1,20 @@
<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="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.WindowCustomizationsPage">
<StackPanel Spacing="10" Margin="25">
<CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
<CheckBox Content="Titlebar" IsChecked="{Binding SystemTitleBarEnabled}" />
<CheckBox Content="System Chrome Buttons" IsChecked="{Binding SystemChromeButtonsEnabled}" />
<CheckBox Content="Managed Chrome Buttons" IsChecked="{Binding ManagedChromeButtonsEnabled}" />
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
<ComboBox x:Name="TransparencyLevels" SelectedIndex="{Binding TransparencyLevel}">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>
<ComboBoxItem>AcrylicBlur</ComboBoxItem>
</ComboBox>
</StackPanel>
</UserControl>

19
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class WindowCustomizationsPage : UserControl
{
public WindowCustomizationsPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

88
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -3,6 +3,8 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
using Avalonia.Platform;
using System;
using ReactiveUI;
namespace ControlCatalog.ViewModels
@ -62,8 +64,94 @@ namespace ControlCatalog.ViewModels
WindowState.Maximized,
WindowState.FullScreen,
};
this.WhenAnyValue(x => x.SystemChromeButtonsEnabled, x=>x.ManagedChromeButtonsEnabled, x => x.SystemTitleBarEnabled)
.Subscribe(x =>
{
var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
if(x.Item1)
{
hints |= ExtendClientAreaChromeHints.SystemChromeButtons;
}
if(x.Item2)
{
hints |= ExtendClientAreaChromeHints.ManagedChromeButtons;
}
if(x.Item3)
{
hints |= ExtendClientAreaChromeHints.SystemTitleBar;
}
ChromeHints = hints;
});
SystemTitleBarEnabled = true;
SystemChromeButtonsEnabled = true;
TitleBarHeight = -1;
}
private int _transparencyLevel;
public int TransparencyLevel
{
get { return _transparencyLevel; }
set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); }
}
private ExtendClientAreaChromeHints _chromeHints;
public ExtendClientAreaChromeHints ChromeHints
{
get { return _chromeHints; }
set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
}
private bool _extendClientAreaEnabled;
public bool ExtendClientAreaEnabled
{
get { return _extendClientAreaEnabled; }
set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
}
private bool _systemTitleBarEnabled;
public bool SystemTitleBarEnabled
{
get { return _systemTitleBarEnabled; }
set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
}
private bool _systemChromeButtonsEnabled;
public bool SystemChromeButtonsEnabled
{
get { return _systemChromeButtonsEnabled; }
set { this.RaiseAndSetIfChanged(ref _systemChromeButtonsEnabled, value); }
}
private bool _managedChromeButtonsEnabled;
public bool ManagedChromeButtonsEnabled
{
get { return _managedChromeButtonsEnabled; }
set { this.RaiseAndSetIfChanged(ref _managedChromeButtonsEnabled, value); }
}
private double _titleBarHeight;
public double TitleBarHeight
{
get { return _titleBarHeight; }
set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); }
}
public WindowState WindowState
{
get { return _windowState; }

31
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -1,5 +1,4 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities
{
@ -174,5 +173,35 @@ namespace Avalonia.Utilities
return val;
}
}
/// <summary>
/// Converts an angle in degrees to radians.
/// </summary>
/// <param name="angle">The angle in degrees.</param>
/// <returns>The angle in radians.</returns>
public static double Deg2Rad(double angle)
{
return angle * (Math.PI / 180d);
}
/// <summary>
/// Converts an angle in gradians to radians.
/// </summary>
/// <param name="angle">The angle in gradians.</param>
/// <returns>The angle in radians.</returns>
public static double Grad2Rad(double angle)
{
return angle * (Math.PI / 200d);
}
/// <summary>
/// Converts an angle in turns to radians.
/// </summary>
/// <param name="angle">The angle in turns.</param>
/// <returns>The angle in radians.</returns>
public static double Turn2Rad(double angle)
{
return angle * 2 * Math.PI;
}
}
}

63
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Text;
using Avalonia.Controls.Primitives;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Chrome
{
public class CaptionButtons : TemplatedControl
{
private CompositeDisposable _disposables;
private Window _hostWindow;
public void Attach(Window hostWindow)
{
if (_disposables == null)
{
_hostWindow = hostWindow;
_disposables = new CompositeDisposable
{
_hostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
})
};
}
}
public void Detach()
{
if (_disposables != null)
{
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow);
layer.Children.Remove(this);
_disposables.Dispose();
_disposables = null;
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var closeButton = e.NameScope.Find<Panel>("PART_CloseButton");
var restoreButton = e.NameScope.Find<Panel>("PART_RestoreButton");
var minimiseButton = e.NameScope.Find<Panel>("PART_MinimiseButton");
var fullScreenButton = e.NameScope.Find<Panel>("PART_FullScreenButton");
closeButton.PointerPressed += (sender, e) => _hostWindow.Close();
restoreButton.PointerPressed += (sender, e) => _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
minimiseButton.PointerPressed += (sender, e) => _hostWindow.WindowState = WindowState.Minimized;
fullScreenButton.PointerPressed += (sender, e) => _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen;
}
}
}

97
src/Avalonia.Controls/Chrome/TitleBar.cs

@ -0,0 +1,97 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
namespace Avalonia.Controls.Chrome
{
public class TitleBar : TemplatedControl
{
private CompositeDisposable _disposables;
private Window _hostWindow;
private CaptionButtons _captionButtons;
public TitleBar(Window hostWindow)
{
_hostWindow = hostWindow;
}
public void Attach()
{
if (_disposables == null)
{
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow);
layer.Children.Add(this);
_disposables = new CompositeDisposable
{
_hostWindow.GetObservable(Window.WindowDecorationMarginsProperty)
.Subscribe(x => InvalidateSize()),
_hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty)
.Subscribe(x => InvalidateSize()),
_hostWindow.GetObservable(Window.OffScreenMarginProperty)
.Subscribe(x => InvalidateSize()),
_hostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
})
};
_captionButtons?.Attach(_hostWindow);
}
}
void InvalidateSize()
{
Margin = new Thickness(
_hostWindow.OffScreenMargin.Left,
_hostWindow.OffScreenMargin.Top,
_hostWindow.OffScreenMargin.Right,
_hostWindow.OffScreenMargin.Bottom);
if (_hostWindow.WindowState != WindowState.FullScreen)
{
Height = _hostWindow.WindowDecorationMargins.Top;
if (_captionButtons != null)
{
_captionButtons.Height = Height;
}
}
}
public void Detach()
{
if (_disposables != null)
{
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow);
layer.Children.Remove(this);
_disposables.Dispose();
_disposables = null;
_captionButtons?.Detach();
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_captionButtons = e.NameScope.Find<CaptionButtons>("PART_CaptionButtons");
_captionButtons.Attach(_hostWindow);
}
}
}

6
src/Avalonia.Controls/LayoutTransformControl.cs

@ -14,8 +14,8 @@ namespace Avalonia.Controls
/// </summary>
public class LayoutTransformControl : Decorator
{
public static readonly StyledProperty<Transform> LayoutTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, Transform>(nameof(LayoutTransform));
public static readonly StyledProperty<ITransform> LayoutTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, ITransform>(nameof(LayoutTransform));
public static readonly StyledProperty<bool> UseRenderTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(LayoutTransform));
@ -37,7 +37,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets a graphics transformation that should apply to this element when layout is performed.
/// </summary>
public Transform LayoutTransform
public ITransform LayoutTransform
{
get { return GetValue(LayoutTransformProperty); }
set { SetValue(LayoutTransformProperty, value); }

21
src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs

@ -0,0 +1,21 @@
using System;
namespace Avalonia.Platform
{
[Flags]
public enum ExtendClientAreaChromeHints
{
NoChrome,
Default = SystemTitleBar,
SystemTitleBar = 0x01,
ManagedChromeButtons = 0x02,
SystemChromeButtons = 0x04,
OSXThickTitleBar = 0x08,
PreferSystemChromeButtons = 0x10,
AdaptiveChromeWithTitleBar = SystemTitleBar | PreferSystemChromeButtons,
AdaptiveChromeWithoutTitleBar = PreferSystemChromeButtons,
}
}

14
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -94,5 +94,19 @@ namespace Avalonia.Platform
/// </summary>
///
void SetMinMaxSize(Size minSize, Size maxSize);
void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint);
bool IsClientAreaExtendedToDecorations { get; }
void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints);
void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight);
Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
Thickness ExtendedMargins { get; }
Thickness OffScreenMargin { get; }
}
}

37
src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs

@ -0,0 +1,37 @@
using System.Linq;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest
{
public Size AvailableSize { get; private set; }
public static ChromeOverlayLayer GetOverlayLayer(IVisual visual)
{
foreach (var v in visual.GetVisualAncestors())
if (v is VisualLayerManager vlm)
if (vlm.OverlayLayer != null)
return vlm.ChromeOverlayLayer;
if (visual is TopLevel tl)
{
var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault();
return layers?.ChromeOverlayLayer;
}
return null;
}
public bool HitTest(Point point) => Children.HitTestCustom(point);
protected override Size ArrangeOverride(Size finalSize)
{
// We are saving it here since child controls might need to know the entire size of the overlay
// and Bounds won't be updated in time
AvailableSize = finalSize;
return base.ArrangeOverride(finalSize);
}
}
}

15
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@ -6,7 +6,9 @@ namespace Avalonia.Controls.Primitives
public class VisualLayerManager : Decorator
{
private const int AdornerZIndex = int.MaxValue - 100;
private const int OverlayZIndex = int.MaxValue - 99;
private const int ChromeZIndex = int.MaxValue - 99;
private const int OverlayZIndex = int.MaxValue - 98;
private ILogicalRoot _logicalRoot;
private readonly List<Control> _layers = new List<Control>();
@ -24,6 +26,17 @@ namespace Avalonia.Controls.Primitives
}
}
public ChromeOverlayLayer ChromeOverlayLayer
{
get
{
var rv = FindLayer<ChromeOverlayLayer>();
if (rv == null)
AddLayer(rv = new ChromeOverlayLayer(), ChromeZIndex);
return rv;
}
}
public OverlayLayer OverlayLayer
{
get

1
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -13,3 +13,4 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")]

2
src/Avalonia.Controls/TopLevel.cs

@ -402,7 +402,7 @@ namespace Avalonia.Controls
}
else
{
_transparencyFallbackBorder.Background = Brushes.Transparent;
_transparencyFallbackBorder.Background = null;
}
}

130
src/Avalonia.Controls/Window.cs

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Chrome;
using Avalonia.Controls.Platform;
using Avalonia.Data;
using Avalonia.Input;
@ -70,6 +71,9 @@ namespace Avalonia.Controls
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot
{
private List<Window> _children = new List<Window>();
private TitleBar _managedTitleBar;
private bool _isExtendedIntoWindowDecorations;
/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
@ -87,6 +91,39 @@ namespace Avalonia.Controls
o => o.HasSystemDecorations,
(o, v) => o.HasSystemDecorations = v);
/// <summary>
/// Defines the <see cref="ExtendClientAreaToDecorationsHint"/> property.
/// </summary>
public static readonly StyledProperty<bool> ExtendClientAreaToDecorationsHintProperty =
AvaloniaProperty.Register<Window, bool>(nameof(ExtendClientAreaToDecorationsHint), false);
public static readonly StyledProperty<ExtendClientAreaChromeHints> ExtendClientAreaChromeHintsProperty =
AvaloniaProperty.Register<Window, ExtendClientAreaChromeHints>(nameof(ExtendClientAreaChromeHints), ExtendClientAreaChromeHints.Default);
public static readonly StyledProperty<double> ExtendClientAreaTitleBarHeightHintProperty =
AvaloniaProperty.Register<Window, double>(nameof(ExtendClientAreaTitleBarHeightHint), -1);
/// <summary>
/// Defines the <see cref="IsExtendedIntoWindowDecorations"/> property.
/// </summary>
public static readonly DirectProperty<Window, bool> IsExtendedIntoWindowDecorationsProperty =
AvaloniaProperty.RegisterDirect<Window, bool>(nameof(IsExtendedIntoWindowDecorations),
o => o.IsExtendedIntoWindowDecorations,
unsetValue: false);
/// <summary>
/// Defines the <see cref="WindowDecorationMargins"/> property.
/// </summary>
public static readonly DirectProperty<Window, Thickness> WindowDecorationMarginsProperty =
AvaloniaProperty.RegisterDirect<Window, Thickness>(nameof(WindowDecorationMargins),
o => o.WindowDecorationMargins);
public static readonly DirectProperty<Window, Thickness> OffScreenMarginProperty =
AvaloniaProperty.RegisterDirect<Window, Thickness>(nameof(OffScreenMargin),
o => o.OffScreenMargin);
/// <summary>
/// Defines the <see cref="SystemDecorations"/> property.
/// </summary>
@ -164,6 +201,23 @@ namespace Avalonia.Controls
WindowStateProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
ExtendClientAreaToDecorationsHintProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaToDecorationsHint((bool)e.NewValue); });
ExtendClientAreaChromeHintsProperty.Changed.AddClassHandler<Window>(
(w, e) =>
{
if (w.PlatformImpl != null)
{
w.PlatformImpl.SetExtendClientAreaChromeHints((ExtendClientAreaChromeHints)e.NewValue);
}
w.HandleChromeHintsChanged((ExtendClientAreaChromeHints)e.NewValue);
});
ExtendClientAreaTitleBarHeightHintProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.SetExtendClientAreaTitleBarHeightHint((double)e.NewValue); });
MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
@ -188,6 +242,7 @@ namespace Avalonia.Controls
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
@ -237,6 +292,52 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets or sets if the ClientArea is Extended into the Window Decorations (chrome or border).
/// </summary>
public bool ExtendClientAreaToDecorationsHint
{
get { return GetValue(ExtendClientAreaToDecorationsHintProperty); }
set { SetValue(ExtendClientAreaToDecorationsHintProperty, value); }
}
public ExtendClientAreaChromeHints ExtendClientAreaChromeHints
{
get => GetValue(ExtendClientAreaChromeHintsProperty);
set => SetValue(ExtendClientAreaChromeHintsProperty, value);
}
public double ExtendClientAreaTitleBarHeightHint
{
get => GetValue(ExtendClientAreaTitleBarHeightHintProperty);
set => SetValue(ExtendClientAreaTitleBarHeightHintProperty, value);
}
/// <summary>
/// Gets if the ClientArea is Extended into the Window Decorations.
/// </summary>
public bool IsExtendedIntoWindowDecorations
{
get => _isExtendedIntoWindowDecorations;
private set => SetAndRaise(IsExtendedIntoWindowDecorationsProperty, ref _isExtendedIntoWindowDecorations, value);
}
private Thickness _windowDecorationMargins;
public Thickness WindowDecorationMargins
{
get => _windowDecorationMargins;
private set => SetAndRaise(WindowDecorationMarginsProperty, ref _windowDecorationMargins, value);
}
private Thickness _offScreenMargin;
public Thickness OffScreenMargin
{
get => _offScreenMargin;
private set => SetAndRaise(OffScreenMarginProperty, ref _offScreenMargin, value);
}
/// <summary>
/// Sets the system decorations (title bar, border, etc)
/// </summary>
@ -438,6 +539,15 @@ namespace Avalonia.Controls
}
}
protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended)
{
IsExtendedIntoWindowDecorations = isExtended;
WindowDecorationMargins = PlatformImpl.ExtendedMargins;
OffScreenMargin = PlatformImpl.OffScreenMargin;
}
/// <summary>
/// Hides the window but does not close it.
/// </summary>
@ -740,6 +850,26 @@ namespace Avalonia.Controls
base.HandleResized(clientSize);
}
private void HandleChromeHintsChanged (ExtendClientAreaChromeHints hints)
{
if(hints.HasFlag(ExtendClientAreaChromeHints.ManagedChromeButtons))
{
if(_managedTitleBar == null)
{
_managedTitleBar = new TitleBar(this);
}
_managedTitleBar.Attach();
}
else
{
if(_managedTitleBar != null)
{
_managedTitleBar.Detach();
}
}
}
/// <summary>
/// Raises the <see cref="Closing"/> event.
/// </summary>

22
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -83,7 +83,15 @@ namespace Avalonia.DesignerSupport.Remote
}
public IScreenImpl Screen { get; } = new ScreenStub();
public Action GotInputWhenDisabled { get; set; }
public Action GotInputWhenDisabled { get; set; }
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Thickness ExtendedMargins { get; } = new Thickness();
public bool IsClientAreaExtendedToDecorations { get; }
public Thickness OffScreenMargin { get; } = new Thickness();
public void Activate()
{
@ -124,5 +132,17 @@ namespace Avalonia.DesignerSupport.Remote
public void SetEnabled(bool enable)
{
}
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
{
}
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
{
}
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
}
}
}

22
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -37,7 +37,13 @@ namespace Avalonia.DesignerSupport.Remote
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Thickness ExtendedMargins { get; } = new Thickness();
public Thickness OffScreenMargin { get; } = new Thickness();
public WindowStub(IWindowImpl parent = null)
{
@ -140,6 +146,18 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
{
}
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
{
}
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
}
public IPopupPositioner PopupPositioner { get; }
public Action GotInputWhenDisabled { get; set; }
@ -151,6 +169,8 @@ namespace Avalonia.DesignerSupport.Remote
}
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public bool IsClientAreaExtendedToDecorations { get; }
}
class ClipboardStub : IClipboard

85
src/Avalonia.Native/WindowImpl.cs

@ -1,6 +1,8 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
@ -14,6 +16,8 @@ namespace Avalonia.Native
private readonly AvaloniaNativePlatformOptions _opts;
private readonly GlPlatformFeature _glFeature;
IAvnWindow _native;
private double _extendTitleBarHeight = -1;
internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts,
GlPlatformFeature glFeature) : base(opts, glFeature)
{
@ -50,6 +54,8 @@ namespace Avalonia.Native
void IAvnWindowEvents.WindowStateChanged(AvnWindowState state)
{
_parent.InvalidateExtendedMargins();
_parent.WindowStateChanged?.Invoke((WindowState)state);
}
@ -96,7 +102,84 @@ namespace Avalonia.Native
}
}
public Action<WindowState> WindowStateChanged { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Thickness ExtendedMargins { get; private set; }
public Thickness OffScreenMargin { get; } = new Thickness();
private bool _isExtended;
public bool IsClientAreaExtendedToDecorations => _isExtended;
protected override bool ChromeHitTest (RawPointerEventArgs e)
{
if(_isExtended)
{
if(e.Type == RawPointerEventType.LeftButtonDown)
{
var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot as Window, x =>
{
if (x is IInputElement ie && !ie.IsHitTestVisible)
{
return false;
}
return true;
});
if(visual == null)
{
_native.BeginMoveDrag();
}
}
}
return false;
}
private void InvalidateExtendedMargins ()
{
if(WindowState == WindowState.FullScreen)
{
ExtendedMargins = new Thickness();
}
else
{
ExtendedMargins = _isExtended ? new Thickness(0, _extendTitleBarHeight == -1 ? _native.GetExtendTitleBarHeight() : _extendTitleBarHeight, 0, 0) : new Thickness();
}
ExtendClientAreaToDecorationsChanged?.Invoke(_isExtended);
}
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
{
_isExtended = extendIntoClientAreaHint;
_native.SetExtendClientArea(extendIntoClientAreaHint);
InvalidateExtendedMargins();
}
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
{
if(hints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChromeButtons))
{
hints |= ExtendClientAreaChromeHints.SystemChromeButtons;
}
_native.SetExtendClientAreaHints ((AvnExtendClientAreaChromeHints)hints);
}
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
_extendTitleBarHeight = titleBarHeight;
_native.SetExtendTitleBarHeight(titleBarHeight);
ExtendedMargins = _isExtended ? new Thickness(0, titleBarHeight == -1 ? _native.GetExtendTitleBarHeight() : titleBarHeight, 0, 0) : new Thickness();
ExtendClientAreaToDecorationsChanged?.Invoke(_isExtended);
}
public void ShowTaskbarIcon(bool value)
{

14
src/Avalonia.Native/WindowImplBase.cs

@ -45,7 +45,7 @@ namespace Avalonia.Native
public abstract class WindowBaseImpl : IWindowBaseImpl,
IFramebufferPlatformSurface
{
IInputRoot _inputRoot;
protected IInputRoot _inputRoot;
IAvnWindowBase _native;
private object _syncRoot = new object();
private bool _deferredRendering = false;
@ -254,6 +254,11 @@ namespace Avalonia.Native
return args.Handled;
}
protected virtual bool ChromeHitTest (RawPointerEventArgs e)
{
return false;
}
public void RawMouseEvent(AvnRawMouseEventType type, uint timeStamp, AvnInputModifiers modifiers, AvnPoint point, AvnVector delta)
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
@ -265,7 +270,12 @@ namespace Avalonia.Native
break;
default:
Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers));
var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers);
if(!ChromeHitTest(e))
{
Input?.Invoke(e);
}
break;
}
}

67
src/Avalonia.Themes.Default/CaptionButtons.xaml

@ -0,0 +1,67 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="CaptionButtons">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="Canvas.Right" Value="0" />
<Setter Property="MaxHeight" Value="30" />
<Setter Property="Template">
<ControlTemplate>
<StackPanel Spacing="2" Margin="0 0 7 0" VerticalAlignment="Stretch" TextBlock.FontSize="10" Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Panel">
<Setter Property="Width" Value="45" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Panel:pointerover">
<Setter Property="Background" Value="#1FFFFFFF" />
</Style>
<Style Selector="Panel#PART_CloseButton:pointerover">
<Setter Property="Background" Value="#AFFF0000" />
</Style>
<Style Selector="Viewbox">
<Setter Property="Width" Value="11" />
<Setter Property="Margin" Value="2" />
</Style>
</StackPanel.Styles>
<Panel x:Name="PART_FullScreenButton">
<Viewbox>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" />
</Viewbox>
</Panel>
<Panel x:Name="PART_MinimiseButton">
<Viewbox>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" Data="M2048 1229v-205h-2048v205h2048z" />
</Viewbox>
</Panel>
<Panel x:Name="PART_RestoreButton">
<Viewbox>
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
</Viewbox.RenderTransform>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}"/>
</Viewbox>
</Panel>
<Panel x:Name="PART_CloseButton">
<Viewbox>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Panel>
</StackPanel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="CaptionButtons Panel#PART_RestoreButton Path">
<Setter Property="Data" Value="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z" />
</Style>
<Style Selector="CaptionButtons:maximized Panel#PART_RestoreButton Path">
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" />
</Style>
<Style Selector="CaptionButtons Panel#PART_FullScreenButton Path">
<Setter Property="Data" Value="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z" />
</Style>
<Style Selector="CaptionButtons:fullscreen Panel#PART_FullScreenButton Path">
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Style>
</Styles>

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -9,6 +9,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.Button.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Carousel.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.CheckBox.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.CaptionButtons.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ComboBox.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ComboBoxItem.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ContentControl.xaml?assembly=Avalonia.Themes.Default"/>

19
src/Avalonia.Themes.Default/TitleBar.xaml

@ -0,0 +1,19 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border>
<TitleBar Background="SkyBlue" Height="30" Width="300" Foreground="Black" />
</Border>
</Design.PreviewWith>
<Style Selector="TitleBar">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="MaxHeight" Value="30" />
<Setter Property="Background" Value="Red" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}" HorizontalAlignment="Stretch">
<CaptionButtons Name="PART_CaptionButtons" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" />
</Border>
</ControlTemplate>
</Setter>
</Style>
</Styles>

4
src/Avalonia.Themes.Default/Window.xaml

@ -5,7 +5,9 @@
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<Panel IsHitTestVisible="False" Margin="{TemplateBinding OffScreenMargin}">
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" Margin="{TemplateBinding WindowDecorationMargins}" />
</Panel>
<Border Background="{TemplateBinding Background}">
<VisualLayerManager>
<ContentPresenter Name="PART_ContentPresenter"

70
src/Avalonia.Themes.Fluent/CaptionButtons.xaml

@ -0,0 +1,70 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="CaptionButtons">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="Canvas.Right" Value="0" />
<Setter Property="MaxHeight" Value="30" />
<Setter Property="Template">
<ControlTemplate>
<StackPanel Spacing="2" Margin="0 0 7 0" VerticalAlignment="Stretch" TextBlock.FontSize="10" Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Panel">
<Setter Property="Width" Value="45" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Panel:pointerover">
<Setter Property="Background" Value="#CFFFFFFF" />
</Style>
<Style Selector="Panel#PART_CloseButton:pointerover">
<Setter Property="Background" Value="#AFFF0000" />
</Style>
<Style Selector="Viewbox">
<Setter Property="Width" Value="11" />
<Setter Property="Margin" Value="2" />
</Style>
</StackPanel.Styles>
<Panel x:Name="PART_FullScreenButton">
<Viewbox>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" />
</Viewbox>
</Panel>
<Panel x:Name="PART_MinimiseButton">
<Viewbox>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" Data="M2048 1229v-205h-2048v205h2048z" />
</Viewbox>
</Panel>
<Panel x:Name="PART_RestoreButton">
<Viewbox>
<Viewbox.RenderTransform>
<RotateTransform Angle="-90" />
</Viewbox.RenderTransform>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}"/>
</Viewbox>
</Panel>
<Panel x:Name="PART_CloseButton">
<Viewbox>
<Path Stretch="UniformToFill" Fill="{TemplateBinding Foreground}" Data="M1169 1024l879 -879l-145 -145l-879 879l-879 -879l-145 145l879 879l-879 879l145 145l879 -879l879 879l145 -145z" />
</Viewbox>
</Panel>
</StackPanel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="CaptionButtons Panel#PART_RestoreButton Path">
<Setter Property="Data" Value="M2048 2048v-2048h-2048v2048h2048zM1843 1843h-1638v-1638h1638v1638z" />
</Style>
<Style Selector="CaptionButtons:maximized Panel#PART_RestoreButton Path">
<Setter Property="Data" Value="M2048 410h-410v-410h-1638v1638h410v410h1638v-1638zM1434 1434h-1229v-1229h1229v1229zM1843 1843h-1229v-205h1024v-1024h205v1229z" />
</Style>
<Style Selector="CaptionButtons Panel#PART_FullScreenButton Path">
<Setter Property="Data" Value="M2048 2048v-819h-205v469l-1493 -1493h469v-205h-819v819h205v-469l1493 1493h-469v205h819z" />
</Style>
<Style Selector="CaptionButtons:fullscreen Panel#PART_FullScreenButton Path">
<Setter Property="Data" Value="M205 1024h819v-819h-205v469l-674 -674l-145 145l674 674h-469v205zM1374 1229h469v-205h-819v819h205v-469l674 674l145 -145z" />
</Style>
<Style Selector="CaptionButtons:fullscreen Panel#PART_RestoreButton, CaptionButtons:fullscreen Panel#PART_MinimiseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
</Styles>

4
src/Avalonia.Themes.Fluent/FluentTheme.xaml

@ -7,7 +7,8 @@
<StyleInclude Source="resm:Avalonia.Themes.Fluent.FocusAdorner.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.Button.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.Carousel.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.CaptionButtons.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.Carousel.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.CheckBox.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.ComboBox.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.ComboBoxItem.xaml?assembly=Avalonia.Themes.Fluent"/>
@ -35,6 +36,7 @@
<StyleInclude Source="resm:Avalonia.Themes.Fluent.TextBox.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.ToggleButton.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.Expander.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.TitleBar.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.TreeView.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.TreeViewItem.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.UserControl.xaml?assembly=Avalonia.Themes.Fluent"/>

53
src/Avalonia.Themes.Fluent/TitleBar.xaml

@ -0,0 +1,53 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border>
<TitleBar Background="SkyBlue" Height="30" Width="300" Foreground="Black" />
</Border>
</Design.PreviewWith>
<Style Selector="TitleBar">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Panel HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="Stretch">
<Panel x:Name="PART_MouseTracker" Height="1" VerticalAlignment="Top" />
<Panel x:Name="PART_Container">
<Border x:Name="PART_Background" Background="{TemplateBinding Background}" />
<CaptionButtons x:Name="PART_CaptionButtons" VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" MaxHeight="30" />
</Panel>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TitleBar:fullscreen">
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
</Style>
<Style Selector="TitleBar /template/ Border#PART_Background">
<Setter Property="IsHitTestVisible" Value="False" />
</Style>
<Style Selector="TitleBar:fullscreen /template/ Border#PART_Background">
<Setter Property="IsHitTestVisible" Value="True" />
</Style>
<Style Selector="TitleBar:fullscreen /template/ Panel#PART_MouseTracker">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="TitleBar:fullscreen /template/ Panel#PART_Container">
<Setter Property="RenderTransform" Value="translateY(-30px)" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:.25" />
</Transitions>
</Setter>
</Style>
<Style Selector="TitleBar:fullscreen:pointerover /template/ Panel#PART_Container">
<Setter Property="RenderTransform" Value="none" />
</Style>
</Styles>

10
src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs

@ -1,6 +1,8 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Media.Transformation;
namespace Avalonia.Animation.Animators
{
@ -19,6 +21,12 @@ namespace Avalonia.Animation.Animators
// Check if the Target Property is Transform derived.
if (typeof(Transform).IsAssignableFrom(Property.OwnerType))
{
if (ctrl.RenderTransform is TransformOperations)
{
// HACK: This animator cannot reasonably animate CSS transforms at the moment.
return Disposable.Empty;
}
if (ctrl.RenderTransform == null)
{
var normalTransform = new TransformGroup();
@ -51,7 +59,7 @@ namespace Avalonia.Animation.Animators
// It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType)
{
return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))

35
src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Transformation;
namespace Avalonia.Animation.Animators
{
public class TransformOperationsAnimator : Animator<ITransform>
{
public TransformOperationsAnimator()
{
Validate = ValidateTransform;
}
private void ValidateTransform(AnimatorKeyFrame kf)
{
if (!(kf.Value is TransformOperations))
{
throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}.");
}
}
public override ITransform Interpolate(double progress, ITransform oldValue, ITransform newValue)
{
var oldTransform = Cast(oldValue);
var newTransform = Cast(newValue);
return TransformOperations.Interpolate(oldTransform, newTransform, progress);
}
private static TransformOperations Cast(ITransform value)
{
return value as TransformOperations ?? TransformOperations.Identity;
}
}
}

25
src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs

@ -0,0 +1,25 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
using Avalonia.Media;
namespace Avalonia.Animation
{
public class TransformOperationsTransition : Transition<ITransform>
{
private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator();
public override IObservable<ITransform> DoTransition(IObservable<double> progress,
ITransform oldValue,
ITransform newValue)
{
return progress
.Select(p =>
{
var f = Easing.Ease(p);
return _operationsAnimator.Interpolate(f, oldValue, newValue);
});
}
}
}

72
src/Avalonia.Visuals/Matrix.cs

@ -9,6 +9,8 @@ namespace Avalonia
/// </summary>
public readonly struct Matrix : IEquatable<Matrix>
{
private const float DecomposeEpsilon = 0.0001f;
private readonly double _m11;
private readonly double _m12;
private readonly double _m21;
@ -54,7 +56,7 @@ namespace Avalonia
/// <summary>
/// HasInverse Property - returns true if this matrix is invertible, false otherwise.
/// </summary>
public bool HasInverse => GetDeterminant() != 0;
public bool HasInverse => Math.Abs(GetDeterminant()) >= double.Epsilon;
/// <summary>
/// The first element of the first row
@ -286,7 +288,7 @@ namespace Avalonia
{
double d = GetDeterminant();
if (d == 0)
if (Math.Abs(d) < double.Epsilon)
{
throw new InvalidOperationException("Transform is not invertible.");
}
@ -319,5 +321,71 @@ namespace Avalonia
);
}
}
public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
{
decomposed = default;
var determinant = matrix.GetDeterminant();
// Based upon constant in System.Numerics.Matrix4x4.
if (Math.Abs(determinant) < DecomposeEpsilon)
{
return false;
}
var m11 = matrix.M11;
var m21 = matrix.M21;
var m12 = matrix.M12;
var m22 = matrix.M22;
// Translation.
decomposed.Translate = new Vector(matrix.M31, matrix.M32);
// Scale sign.
var scaleX = 1d;
var scaleY = 1d;
if (determinant < 0)
{
if (m11 < m22)
{
scaleX *= -1d;
}
else
{
scaleY *= -1d;
}
}
// X Scale.
scaleX *= Math.Sqrt(m11 * m11 + m12 * m12);
m11 /= scaleX;
m12 /= scaleX;
// XY Shear.
double scaledShear = m11 * m21 + m12 * m22;
m21 -= m11 * scaledShear;
m22 -= m12 * scaledShear;
// Y Scale.
scaleY *= Math.Sqrt(m21 * m21 + m22 * m22);
decomposed.Scale = new Vector(scaleX, scaleY);
decomposed.Skew = new Vector(scaledShear / scaleY, 0d);
decomposed.Angle = Math.Atan2(m12, m11);
return true;
}
public struct Decomposed
{
public Vector Translate;
public Vector Scale;
public Vector Skew;
public double Angle;
}
}
}

12
src/Avalonia.Visuals/Media/IMutableTransform.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Media
{
public interface IMutableTransform : ITransform
{
/// <summary>
/// Raised when the transform changes.
/// </summary>
event EventHandler Changed;
}
}

10
src/Avalonia.Visuals/Media/ITransform.cs

@ -0,0 +1,10 @@
using System.ComponentModel;
namespace Avalonia.Media
{
[TypeConverter(typeof(TransformConverter))]
public interface ITransform
{
Matrix Value { get; }
}
}

5
src/Avalonia.Visuals/Media/Transform.cs

@ -8,11 +8,12 @@ namespace Avalonia.Media
/// <summary>
/// Represents a transform on an <see cref="IVisual"/>.
/// </summary>
public abstract class Transform : Animatable
public abstract class Transform : Animatable, IMutableTransform
{
static Transform()
{
Animation.Animation.RegisterAnimator<TransformAnimator>(prop => typeof(Transform).IsAssignableFrom(prop.OwnerType));
Animation.Animation.RegisterAnimator<TransformAnimator>(prop =>
typeof(ITransform).IsAssignableFrom(prop.OwnerType));
}
/// <summary>

23
src/Avalonia.Visuals/Media/TransformConverter.cs

@ -0,0 +1,23 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Media.Transformation;
namespace Avalonia.Media
{
/// <summary>
/// Creates an <see cref="ITransform"/> from a string representation.
/// </summary>
public class TransformConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return TransformOperations.Parse((string)value);
}
}
}

40
src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs

@ -0,0 +1,40 @@
namespace Avalonia.Media.Transformation
{
internal static class InterpolationUtilities
{
public static double InterpolateScalars(double from, double to, double progress)
{
return from * (1d - progress) + to * progress;
}
public static Vector InterpolateVectors(Vector from, Vector to, double progress)
{
var x = InterpolateScalars(from.X, to.X, progress);
var y = InterpolateScalars(from.Y, to.Y, progress);
return new Vector(x, y);
}
public static Matrix ComposeTransform(Matrix.Decomposed decomposed)
{
// According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix
return Matrix.CreateTranslation(decomposed.Translate) *
Matrix.CreateRotation(decomposed.Angle) *
Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) *
Matrix.CreateScale(decomposed.Scale);
}
public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres)
{
Matrix.Decomposed result = default;
result.Translate = InterpolateVectors(from.Translate, to.Translate, progres);
result.Scale = InterpolateVectors(from.Scale, to.Scale, progres);
result.Skew = InterpolateVectors(from.Skew, to.Skew, progres);
result.Angle = InterpolateScalars(from.Angle, to.Angle, progres);
return result;
}
}
}

203
src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs

@ -0,0 +1,203 @@
using System.Runtime.InteropServices;
namespace Avalonia.Media.Transformation
{
public struct TransformOperation
{
public OperationType Type;
public Matrix Matrix;
public DataLayout Data;
public enum OperationType
{
Translate,
Rotate,
Scale,
Skew,
Matrix,
Identity
}
public bool IsIdentity => Matrix.IsIdentity;
public void Bake()
{
Matrix = Matrix.Identity;
switch (Type)
{
case OperationType.Translate:
{
Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y);
break;
}
case OperationType.Rotate:
{
Matrix = Matrix.CreateRotation(Data.Rotate.Angle);
break;
}
case OperationType.Scale:
{
Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y);
break;
}
case OperationType.Skew:
{
Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y);
break;
}
}
}
public static bool IsOperationIdentity(ref TransformOperation? operation)
{
return !operation.HasValue || operation.Value.IsIdentity;
}
public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress,
ref TransformOperation result)
{
bool fromIdentity = IsOperationIdentity(ref from);
bool toIdentity = IsOperationIdentity(ref to);
if (fromIdentity && toIdentity)
{
return true;
}
TransformOperation fromValue = fromIdentity ? default : from.Value;
TransformOperation toValue = toIdentity ? default : to.Value;
var interpolationType = toIdentity ? fromValue.Type : toValue.Type;
result.Type = interpolationType;
switch (interpolationType)
{
case OperationType.Translate:
{
double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X;
double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y;
double toX = toIdentity ? 0 : toValue.Data.Translate.X;
double toY = toIdentity ? 0 : toValue.Data.Translate.Y;
result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
result.Bake();
break;
}
case OperationType.Rotate:
{
double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle;
double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle;
result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress);
result.Bake();
break;
}
case OperationType.Scale:
{
double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X;
double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y;
double toX = toIdentity ? 1 : toValue.Data.Scale.X;
double toY = toIdentity ? 1 : toValue.Data.Scale.Y;
result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
result.Bake();
break;
}
case OperationType.Skew:
{
double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X;
double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y;
double toX = toIdentity ? 0 : toValue.Data.Skew.X;
double toY = toIdentity ? 0 : toValue.Data.Skew.Y;
result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
result.Bake();
break;
}
case OperationType.Matrix:
{
var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix;
var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix;
if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) ||
!Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed))
{
return false;
}
var interpolated =
InterpolationUtilities.InterpolateDecomposedTransforms(
ref fromDecomposed, ref toDecomposed,
progress);
result.Matrix = InterpolationUtilities.ComposeTransform(interpolated);
break;
}
case OperationType.Identity:
{
// Do nothing.
break;
}
}
return true;
}
[StructLayout(LayoutKind.Explicit)]
public struct DataLayout
{
[FieldOffset(0)] public SkewLayout Skew;
[FieldOffset(0)] public ScaleLayout Scale;
[FieldOffset(0)] public TranslateLayout Translate;
[FieldOffset(0)] public RotateLayout Rotate;
public struct SkewLayout
{
public double X;
public double Y;
}
public struct ScaleLayout
{
public double X;
public double Y;
}
public struct TranslateLayout
{
public double X;
public double Y;
}
public struct RotateLayout
{
public double Angle;
}
}
}
}

252
src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs

@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Avalonia.Media.Transformation
{
public sealed class TransformOperations : ITransform
{
public static TransformOperations Identity { get; } = new TransformOperations(new List<TransformOperation>());
private readonly List<TransformOperation> _operations;
private TransformOperations(List<TransformOperation> operations)
{
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
IsIdentity = CheckIsIdentity();
Value = ApplyTransforms();
}
public bool IsIdentity { get; }
public IReadOnlyList<TransformOperation> Operations => _operations;
public Matrix Value { get; }
public static TransformOperations Parse(string s)
{
return TransformParser.Parse(s);
}
public static Builder CreateBuilder(int capacity)
{
return new Builder(capacity);
}
public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress)
{
TransformOperations result = Identity;
if (!TryInterpolate(from, to, progress, ref result))
{
// If the matrices cannot be interpolated, fallback to discrete animation logic.
// See https://drafts.csswg.org/css-transforms/#matrix-interpolation
result = progress < 0.5 ? from : to;
}
return result;
}
private Matrix ApplyTransforms(int startOffset = 0)
{
Matrix matrix = Matrix.Identity;
for (var i = startOffset; i < _operations.Count; i++)
{
TransformOperation operation = _operations[i];
matrix *= operation.Matrix;
}
return matrix;
}
private bool CheckIsIdentity()
{
foreach (TransformOperation operation in _operations)
{
if (!operation.IsIdentity)
{
return false;
}
}
return true;
}
private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result)
{
bool fromIdentity = from.IsIdentity;
bool toIdentity = to.IsIdentity;
if (fromIdentity && toIdentity)
{
return true;
}
int matchingPrefixLength = ComputeMatchingPrefixLength(from, to);
int fromSize = fromIdentity ? 0 : from._operations.Count;
int toSize = toIdentity ? 0 : to._operations.Count;
int numOperations = Math.Max(fromSize, toSize);
var builder = new Builder(matchingPrefixLength);
for (int i = 0; i < matchingPrefixLength; i++)
{
TransformOperation interpolated = new TransformOperation
{
Type = TransformOperation.OperationType.Identity
};
if (!TransformOperation.TryInterpolate(
i >= fromSize ? default(TransformOperation?) : from._operations[i],
i >= toSize ? default(TransformOperation?) : to._operations[i],
progress,
ref interpolated))
{
return false;
}
builder.Append(interpolated);
}
if (matchingPrefixLength < numOperations)
{
if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) ||
!ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed))
{
return false;
}
var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress);
builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform));
}
result = builder.Build();
return true;
}
private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed)
{
Matrix transform = operations.ApplyTransforms(startOffset);
if (!Matrix.TryDecomposeTransform(transform, out decomposed))
{
return false;
}
return true;
}
private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to)
{
int numOperations = Math.Min(from._operations.Count, to._operations.Count);
for (int i = 0; i < numOperations; i++)
{
if (from._operations[i].Type != to._operations[i].Type)
{
return i;
}
}
// If the operations match to the length of the shorter list, then pad its
// length with the matching identity operations.
// https://drafts.csswg.org/css-transforms/#transform-function-lists
return Math.Max(from._operations.Count, to._operations.Count);
}
public readonly struct Builder
{
private readonly List<TransformOperation> _operations;
public Builder(int capacity)
{
_operations = new List<TransformOperation>(capacity);
}
public void AppendTranslate(double x, double y)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Translate;
toAdd.Data.Translate.X = x;
toAdd.Data.Translate.Y = y;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendRotate(double angle)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Rotate;
toAdd.Data.Rotate.Angle = angle;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendScale(double x, double y)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Scale;
toAdd.Data.Scale.X = x;
toAdd.Data.Scale.Y = y;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendSkew(double x, double y)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Skew;
toAdd.Data.Skew.X = x;
toAdd.Data.Skew.Y = y;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendMatrix(Matrix matrix)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Matrix;
toAdd.Matrix = matrix;
_operations.Add(toAdd);
}
public void AppendIdentity()
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Identity;
_operations.Add(toAdd);
}
public void Append(TransformOperation toAdd)
{
_operations.Add(toAdd);
}
public TransformOperations Build()
{
return new TransformOperations(_operations);
}
}
}
}

463
src/Avalonia.Visuals/Media/Transformation/TransformParser.cs

@ -0,0 +1,463 @@
using System;
using System.Globalization;
using Avalonia.Utilities;
namespace Avalonia.Media.Transformation
{
public static class TransformParser
{
private static readonly (string, TransformFunction)[] s_functionMapping =
{
("translate", TransformFunction.Translate),
("translateX", TransformFunction.TranslateX),
("translateY", TransformFunction.TranslateY),
("scale", TransformFunction.Scale),
("scaleX", TransformFunction.ScaleX),
("scaleY", TransformFunction.ScaleY),
("skew", TransformFunction.Skew),
("skewX", TransformFunction.SkewX),
("skewY", TransformFunction.SkewY),
("rotate", TransformFunction.Rotate),
("matrix", TransformFunction.Matrix)
};
private static readonly (string, Unit)[] s_unitMapping =
{
("deg", Unit.Degree),
("grad", Unit.Gradian),
("rad", Unit.Radian),
("turn", Unit.Turn),
("px", Unit.Pixel)
};
public static TransformOperations Parse(string s)
{
void ThrowInvalidFormat()
{
throw new FormatException($"Invalid transform string: '{s}'.");
}
if (string.IsNullOrEmpty(s))
{
throw new ArgumentException(nameof(s));
}
var span = s.AsSpan().Trim();
if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return TransformOperations.Identity;
}
var builder = TransformOperations.CreateBuilder(0);
while (true)
{
var beginIndex = span.IndexOf('(');
var endIndex = span.IndexOf(')');
if (beginIndex == -1 || endIndex == -1)
{
ThrowInvalidFormat();
}
var namePart = span.Slice(0, beginIndex).Trim();
var function = ParseTransformFunction(in namePart);
if (function == TransformFunction.Invalid)
{
ThrowInvalidFormat();
}
var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim();
ParseFunction(in valuePart, function, in builder);
span = span.Slice(endIndex + 1);
if (span.IsWhiteSpace())
{
break;
}
}
return builder.Build();
}
private static void ParseFunction(
in ReadOnlySpan<char> functionPart,
TransformFunction function,
in TransformOperations.Builder builder)
{
static UnitValue ParseValue(ReadOnlySpan<char> part)
{
int unitIndex = -1;
for (int i = 0; i < part.Length; i++)
{
char c = part[i];
if (char.IsDigit(c) || c == '-' || c == '.')
{
continue;
}
unitIndex = i;
break;
}
Unit unit = Unit.None;
if (unitIndex != -1)
{
var unitPart = part.Slice(unitIndex, part.Length - unitIndex);
unit = ParseUnit(unitPart);
part = part.Slice(0, unitIndex);
}
var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
return new UnitValue(unit, value);
}
static int ParseValuePair(
in ReadOnlySpan<char> part,
ref UnitValue leftValue,
ref UnitValue rightValue)
{
var commaIndex = part.IndexOf(',');
if (commaIndex != -1)
{
var leftPart = part.Slice(0, commaIndex).Trim();
var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim();
leftValue = ParseValue(leftPart);
rightValue = ParseValue(rightPart);
return 2;
}
leftValue = ParseValue(part);
return 1;
}
static int ParseCommaDelimitedValues(ReadOnlySpan<char> part, in Span<UnitValue> outValues)
{
int valueIndex = 0;
while (true)
{
if (valueIndex >= outValues.Length)
{
throw new FormatException("Too many provided values.");
}
var commaIndex = part.IndexOf(',');
if (commaIndex == -1)
{
if (!part.IsWhiteSpace())
{
outValues[valueIndex++] = ParseValue(part);
}
break;
}
var valuePart = part.Slice(0, commaIndex).Trim();
outValues[valueIndex++] = ParseValue(valuePart);
part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1);
}
return valueIndex;
}
switch (function)
{
case TransformFunction.Scale:
case TransformFunction.ScaleX:
case TransformFunction.ScaleY:
{
var scaleX = UnitValue.One;
var scaleY = UnitValue.One;
int count = ParseValuePair(functionPart, ref scaleX, ref scaleY);
if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY))
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrUnit(function, in scaleX, Unit.None);
VerifyZeroOrUnit(function, in scaleY, Unit.None);
if (function == TransformFunction.ScaleX)
{
scaleY = UnitValue.Zero;
}
else if (function == TransformFunction.ScaleY)
{
scaleY = scaleX;
scaleX = UnitValue.Zero;
}
else if (count == 1)
{
scaleY = scaleX;
}
builder.AppendScale(scaleX.Value, scaleY.Value);
break;
}
case TransformFunction.Skew:
case TransformFunction.SkewX:
case TransformFunction.SkewY:
{
var skewX = UnitValue.Zero;
var skewY = UnitValue.Zero;
int count = ParseValuePair(functionPart, ref skewX, ref skewY);
if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY))
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrAngle(function, in skewX);
VerifyZeroOrAngle(function, in skewY);
if (function == TransformFunction.SkewX)
{
skewY = UnitValue.Zero;
}
else if (function == TransformFunction.SkewY)
{
skewY = skewX;
skewX = UnitValue.Zero;
}
else if (count == 1)
{
skewY = skewX;
}
builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY));
break;
}
case TransformFunction.Rotate:
{
var angle = UnitValue.Zero;
UnitValue _ = default;
int count = ParseValuePair(functionPart, ref angle, ref _);
if (count != 1)
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrAngle(function, in angle);
builder.AppendRotate(ToRadians(in angle));
break;
}
case TransformFunction.Translate:
case TransformFunction.TranslateX:
case TransformFunction.TranslateY:
{
var translateX = UnitValue.Zero;
var translateY = UnitValue.Zero;
int count = ParseValuePair(functionPart, ref translateX, ref translateY);
if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY))
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrUnit(function, in translateX, Unit.Pixel);
VerifyZeroOrUnit(function, in translateY, Unit.Pixel);
if (function == TransformFunction.TranslateX)
{
translateY = UnitValue.Zero;
}
else if (function == TransformFunction.TranslateY)
{
translateY = translateX;
translateX = UnitValue.Zero;
}
else if (count == 1)
{
translateY = translateX;
}
builder.AppendTranslate(translateX.Value, translateY.Value);
break;
}
case TransformFunction.Matrix:
{
Span<UnitValue> values = stackalloc UnitValue[6];
int count = ParseCommaDelimitedValues(functionPart, in values);
if (count != 6)
{
ThrowFormatInvalidValueCount(function, 6);
}
foreach (UnitValue value in values)
{
VerifyZeroOrUnit(function, value, Unit.None);
}
var matrix = new Matrix(
values[0].Value,
values[1].Value,
values[2].Value,
values[3].Value,
values[4].Value,
values[5].Value);
builder.AppendMatrix(matrix);
break;
}
}
}
private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit)
{
bool isZero = value.Unit == Unit.None && value.Value == 0d;
if (!isZero && value.Unit != unit)
{
ThrowFormatInvalidValue(function, in value);
}
}
private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value)
{
if (value.Value != 0d && !IsAngleUnit(value.Unit))
{
ThrowFormatInvalidValue(function, in value);
}
}
private static bool IsAngleUnit(Unit unit)
{
switch (unit)
{
case Unit.Radian:
case Unit.Degree:
case Unit.Turn:
{
return true;
}
}
return false;
}
private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value)
{
var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString();
throw new FormatException($"Invalid value {value.Value} {unitString} for {function}");
}
private static void ThrowFormatInvalidValueCount(TransformFunction function, int count)
{
throw new FormatException($"Invalid format. {function} expects {count} value(s).");
}
private static Unit ParseUnit(in ReadOnlySpan<char> part)
{
foreach (var (name, unit) in s_unitMapping)
{
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return unit;
}
}
throw new FormatException($"Invalid unit: {part.ToString()}");
}
private static TransformFunction ParseTransformFunction(in ReadOnlySpan<char> part)
{
foreach (var (name, transformFunction) in s_functionMapping)
{
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return transformFunction;
}
}
return TransformFunction.Invalid;
}
private static double ToRadians(in UnitValue value)
{
return value.Unit switch
{
Unit.Radian => value.Value,
Unit.Gradian => MathUtilities.Grad2Rad(value.Value),
Unit.Degree => MathUtilities.Deg2Rad(value.Value),
Unit.Turn => MathUtilities.Turn2Rad(value.Value),
_ => value.Value
};
}
private enum Unit
{
None,
Pixel,
Radian,
Gradian,
Degree,
Turn
}
private readonly struct UnitValue
{
public readonly Unit Unit;
public readonly double Value;
public UnitValue(Unit unit, double value)
{
Unit = unit;
Value = value;
}
public static UnitValue Zero => new UnitValue(Unit.None, 0);
public static UnitValue One => new UnitValue(Unit.None, 1);
}
private enum TransformFunction
{
Invalid,
Translate,
TranslateX,
TranslateY,
Scale,
ScaleX,
ScaleY,
Skew,
SkewX,
SkewY,
Rotate,
Matrix
}
}
}

1
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]

14
src/Avalonia.Visuals/Visual.cs

@ -68,8 +68,8 @@ namespace Avalonia
/// <summary>
/// Defines the <see cref="RenderTransform"/> property.
/// </summary>
public static readonly StyledProperty<Transform> RenderTransformProperty =
AvaloniaProperty.Register<Visual, Transform>(nameof(RenderTransform));
public static readonly StyledProperty<ITransform> RenderTransformProperty =
AvaloniaProperty.Register<Visual, ITransform>(nameof(RenderTransform));
/// <summary>
/// Defines the <see cref="RenderTransformOrigin"/> property.
@ -219,7 +219,7 @@ namespace Avalonia
/// <summary>
/// Gets the render transform of the control.
/// </summary>
public Transform RenderTransform
public ITransform RenderTransform
{
get { return GetValue(RenderTransformProperty); }
set { SetValue(RenderTransformProperty, value); }
@ -391,9 +391,9 @@ namespace Avalonia
_visualRoot = e.Root;
if (RenderTransform != null)
if (RenderTransform is IMutableTransform mutableTransform)
{
RenderTransform.Changed += RenderTransformChanged;
mutableTransform.Changed += RenderTransformChanged;
}
EnableTransitions();
@ -428,9 +428,9 @@ namespace Avalonia
_visualRoot = null;
if (RenderTransform != null)
if (RenderTransform is IMutableTransform mutableTransform)
{
RenderTransform.Changed -= RenderTransformChanged;
mutableTransform.Changed -= RenderTransformChanged;
}
DisableTransitions();

2
src/Avalonia.Visuals/VisualTree/IVisual.cs

@ -76,7 +76,7 @@ namespace Avalonia.VisualTree
/// <summary>
/// Gets or sets the render transform of the control.
/// </summary>
Transform RenderTransform { get; set; }
ITransform RenderTransform { get; set; }
/// <summary>
/// Gets or sets the render transform origin of the control.

22
src/Avalonia.X11/X11Window.cs

@ -309,7 +309,15 @@ namespace Avalonia.X11
{
get => _transparencyHelper.TransparencyLevelChanged;
set => _transparencyHelper.TransparencyLevelChanged = value;
}
}
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Thickness ExtendedMargins { get; } = new Thickness();
public Thickness OffScreenMargin { get; } = new Thickness();
public bool IsClientAreaExtendedToDecorations { get; }
public Action Closed { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
@ -1035,6 +1043,18 @@ namespace Avalonia.X11
_disabled = !enable;
}
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
{
}
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
{
}
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
}
public Action GotInputWhenDisabled { get; set; }
public void SetIcon(IWindowIconImpl icon)

9
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -989,6 +989,12 @@ namespace Avalonia.Win32.Interop
}
}
[DllImport("user32.dll")]
public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl);
@ -1311,6 +1317,9 @@ namespace Avalonia.Win32.Interop
[DllImport("dwmapi.dll")]
public static extern int DwmIsCompositionEnabled(out bool enabled);
[DllImport("dwmapi.dll")]
public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult);
[DllImport("dwmapi.dll")]
public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind);

526
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -0,0 +1,526 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Win32.Input;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32
{
public partial class WindowImpl
{
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
Justification = "Using Win32 naming for consistency.")]
protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
const double wheelDelta = 120.0;
uint timestamp = unchecked((uint)GetMessageTime());
RawInputEventArgs e = null;
switch ((WindowsMessage)msg)
{
case WindowsMessage.WM_ACTIVATE:
{
var wa = (WindowActivate)(ToInt32(wParam) & 0xffff);
switch (wa)
{
case WindowActivate.WA_ACTIVE:
case WindowActivate.WA_CLICKACTIVE:
{
Activated?.Invoke();
break;
}
case WindowActivate.WA_INACTIVE:
{
Deactivated?.Invoke();
break;
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_NCCALCSIZE:
{
if (ToInt32(wParam) == 1 && !HasFullDecorations || _isClientAreaExtended)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_CLOSE:
{
bool? preventClosing = Closing?.Invoke();
if (preventClosing == true)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_DESTROY:
{
//Window doesn't exist anymore
_hwnd = IntPtr.Zero;
//Remove root reference to this class, so unmanaged delegate can be collected
s_instances.Remove(this);
Closed?.Invoke();
_mouseDevice.Dispose();
_touchDevice?.Dispose();
//Free other resources
Dispose();
return IntPtr.Zero;
}
case WindowsMessage.WM_DPICHANGED:
{
var dpi = ToInt32(wParam) & 0xffff;
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = dpi / 96.0;
ScalingChanged?.Invoke(_scaling);
SetWindowPos(hWnd,
IntPtr.Zero,
newDisplayRect.left,
newDisplayRect.top,
newDisplayRect.right - newDisplayRect.left,
newDisplayRect.bottom - newDisplayRect.top,
SetWindowPosFlags.SWP_NOZORDER |
SetWindowPosFlags.SWP_NOACTIVATE);
return IntPtr.Zero;
}
case WindowsMessage.WM_KEYDOWN:
case WindowsMessage.WM_SYSKEYDOWN:
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
_owner,
RawKeyEventType.KeyDown,
KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_MENUCHAR:
{
// mute the system beep
return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16);
}
case WindowsMessage.WM_KEYUP:
case WindowsMessage.WM_SYSKEYUP:
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
_owner,
RawKeyEventType.KeyUp,
KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_CHAR:
{
// Ignore control chars
if (ToInt32(wParam) >= 32)
{
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner,
new string((char)ToInt32(wParam), 1));
}
break;
}
case WindowsMessage.WM_LBUTTONDOWN:
case WindowsMessage.WM_RBUTTONDOWN:
case WindowsMessage.WM_MBUTTONDOWN:
case WindowsMessage.WM_XBUTTONDOWN:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
WindowsMessage.WM_XBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Down :
RawPointerEventType.XButton2Down
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_LBUTTONUP:
case WindowsMessage.WM_RBUTTONUP:
case WindowsMessage.WM_MBUTTONUP:
case WindowsMessage.WM_XBUTTONUP:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp,
WindowsMessage.WM_XBUTTONUP =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Up :
RawPointerEventType.XButton2Up,
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEMOVE:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
if (!_trackingMouse)
{
var tm = new TRACKMOUSEEVENT
{
cbSize = Marshal.SizeOf<TRACKMOUSEEVENT>(),
dwFlags = 2,
hwndTrack = _hwnd,
dwHoverTime = 0,
};
TrackMouseEvent(ref tm);
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.Move,
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEWHEEL:
{
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEHWHEEL:
{
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSELEAVE:
{
_trackingMouse = false;
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.LeaveWindow,
new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_NCLBUTTONDOWN:
case WindowsMessage.WM_NCRBUTTONDOWN:
case WindowsMessage.WM_NCMBUTTONDOWN:
case WindowsMessage.WM_NCXBUTTONDOWN:
{
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType
.NonClientLeftButtonDown,
WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown,
WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
WindowsMessage.WM_NCXBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Down :
RawPointerEventType.XButton2Down,
},
PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_TOUCH:
{
var touchInputCount = wParam.ToInt32();
var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
var touchInputs = new Span<TOUCHINPUT>(pTouchInputs, touchInputCount);
if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf<TOUCHINPUT>()))
{
foreach (var touchInput in touchInputs)
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id));
}
CloseTouchInputHandle(lParam);
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCPAINT:
{
if (!HasFullDecorations)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCACTIVATE:
{
if (!HasFullDecorations)
{
return new IntPtr(1);
}
break;
}
case WindowsMessage.WM_PAINT:
{
using (_rendererLock.Lock())
{
if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero)
{
var f = Scaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f,
(r.bottom - r.top) / f));
EndPaint(_hwnd, ref ps);
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_SIZE:
{
using (_rendererLock.Lock())
{
// Do nothing here, just block until the pending frame render is completed on the render thread
}
var size = (SizeCommand)wParam;
if (Resized != null &&
(size == SizeCommand.Restored ||
size == SizeCommand.Maximized))
{
var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16);
Resized(clientSize / Scaling);
}
var windowState = size == SizeCommand.Maximized ?
WindowState.Maximized :
(size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal);
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowStateChanged?.Invoke(windowState);
if (_isClientAreaExtended)
{
UpdateExtendMargins();
ExtendClientAreaToDecorationsChanged?.Invoke(true);
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_MOVE:
{
PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff),
(short)(ToInt32(lParam) >> 16)));
return IntPtr.Zero;
}
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
if (_minSize.Width > 0)
{
mmi.ptMinTrackSize.X =
(int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right);
}
if (_minSize.Height > 0)
{
mmi.ptMinTrackSize.Y =
(int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom);
}
if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0)
{
mmi.ptMaxTrackSize.X =
(int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right);
}
if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0)
{
mmi.ptMaxTrackSize.Y =
(int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom);
}
Marshal.StructureToPtr(mmi, lParam, true);
return IntPtr.Zero;
}
case WindowsMessage.WM_DISPLAYCHANGE:
{
(Screen as ScreenImpl)?.InvalidateScreensCache();
return IntPtr.Zero;
}
}
#if USE_MANAGED_DRAG
if (_managedDrag.PreprocessInputEvent(ref e))
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
#endif
if (e != null && Input != null)
{
Input(e);
if (e.Handled)
{
return IntPtr.Zero;
}
}
using (_rendererLock.Lock())
{
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
private static int ToInt32(IntPtr ptr)
{
if (IntPtr.Size == 4)
return ptr.ToInt32();
return (int)(ptr.ToInt64() & 0xffffffff);
}
private static int HighWord(int param) => param >> 16;
private Point DipFromLParam(IntPtr lParam)
{
return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling;
}
private PixelPoint PointFromLParam(IntPtr lParam)
{
return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16));
}
private bool ShouldIgnoreTouchEmulatedMessage()
{
if (!_multitouch)
{
return false;
}
// MI_WP_SIGNATURE
// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
const long marker = 0xFF515700L;
var info = GetMessageExtraInfo().ToInt64();
return (info & marker) == marker;
}
private static RawInputModifiers GetMouseModifiers(IntPtr wParam)
{
var keys = (ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON))
{
modifiers |= RawInputModifiers.LeftMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON))
{
modifiers |= RawInputModifiers.RightMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON))
{
modifiers |= RawInputModifiers.MiddleMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1))
{
modifiers |= RawInputModifiers.XButton1MouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2))
{
modifiers |= RawInputModifiers.XButton2MouseButton;
}
return modifiers;
}
}
}

135
src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs

@ -0,0 +1,135 @@
using System;
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Input;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32
{
public partial class WindowImpl
{
// Hit test the frame for resizing and moving.
HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam)
{
// Get the point coordinates for the hit test.
var ptMouse = PointFromLParam(lParam);
// Get the window rectangle.
GetWindowRect(hWnd, out var rcWindow);
// Get the frame rectangle, adjusted for the style without a caption.
RECT rcFrame = new RECT();
AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
RECT border_thickness = new RECT();
if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME))
{
AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0);
border_thickness.left *= -1;
border_thickness.top *= -1;
}
else if (GetStyle().HasFlag(WindowStyles.WS_BORDER))
{
border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 };
}
if (_extendTitleBarHint >= 0)
{
border_thickness.top = (int)(_extendedMargins.Top * Scaling);
}
// Determine if the hit test is for resizing. Default middle (1,1).
ushort uRow = 1;
ushort uCol = 1;
bool fOnResizeBorder = false;
// Determine if the point is at the top or bottom of the window.
if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + border_thickness.top)
{
fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top));
uRow = 0;
}
else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - border_thickness.bottom)
{
uRow = 2;
}
// Determine if the point is at the left or right of the window.
if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + border_thickness.left)
{
uCol = 0; // left side
}
else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - border_thickness.right)
{
uCol = 2; // right side
}
// Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT)
HitTestValues[][] hitTests = new[]
{
new []{ HitTestValues.HTTOPLEFT, fOnResizeBorder ? HitTestValues.HTTOP : HitTestValues.HTCAPTION, HitTestValues.HTTOPRIGHT },
new []{ HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT },
new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT },
};
return hitTests[uRow][uCol];
}
protected virtual IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp)
{
IntPtr lRet = IntPtr.Zero;
callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet);
switch ((WindowsMessage)msg)
{
case WindowsMessage.WM_DWMCOMPOSITIONCHANGED:
// TODO handle composition changed.
break;
case WindowsMessage.WM_NCHITTEST:
if (lRet == IntPtr.Zero)
{
if(WindowState == WindowState.FullScreen)
{
return (IntPtr)HitTestValues.HTCLIENT;
}
var hittestResult = HitTestNCA(hWnd, wParam, lParam);
lRet = (IntPtr)hittestResult;
uint timestamp = unchecked((uint)GetMessageTime());
if (hittestResult == HitTestValues.HTCAPTION)
{
var position = PointToClient(PointFromLParam(lParam));
var visual = (_owner as Window).Renderer.HitTestFirst(position, _owner as Window, x =>
{
if (x is IInputElement ie && !ie.IsHitTestVisible)
{
return false;
}
return true;
});
if (visual != null)
{
hittestResult = HitTestValues.HTCLIENT;
lRet = (IntPtr)hittestResult;
}
}
if (hittestResult != HitTestValues.HTNOWHERE)
{
callDwp = false;
}
}
break;
}
return lRet;
}
}
}

500
src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs

@ -14,508 +14,22 @@ namespace Avalonia.Win32
{
public partial class WindowImpl
{
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
Justification = "Using Win32 naming for consistency.")]
protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
const double wheelDelta = 120.0;
uint timestamp = unchecked((uint)GetMessageTime());
IntPtr lRet = IntPtr.Zero;
bool callDwp = true;
RawInputEventArgs e = null;
switch ((WindowsMessage)msg)
{
case WindowsMessage.WM_ACTIVATE:
{
var wa = (WindowActivate)(ToInt32(wParam) & 0xffff);
switch (wa)
{
case WindowActivate.WA_ACTIVE:
case WindowActivate.WA_CLICKACTIVE:
{
Activated?.Invoke();
break;
}
case WindowActivate.WA_INACTIVE:
{
Deactivated?.Invoke();
break;
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_NCCALCSIZE:
{
if (ToInt32(wParam) == 1 && !HasFullDecorations)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_CLOSE:
{
bool? preventClosing = Closing?.Invoke();
if (preventClosing == true)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_DESTROY:
{
//Window doesn't exist anymore
_hwnd = IntPtr.Zero;
//Remove root reference to this class, so unmanaged delegate can be collected
s_instances.Remove(this);
Closed?.Invoke();
_mouseDevice.Dispose();
_touchDevice?.Dispose();
//Free other resources
Dispose();
return IntPtr.Zero;
}
case WindowsMessage.WM_DPICHANGED:
{
var dpi = ToInt32(wParam) & 0xffff;
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = dpi / 96.0;
ScalingChanged?.Invoke(_scaling);
SetWindowPos(hWnd,
IntPtr.Zero,
newDisplayRect.left,
newDisplayRect.top,
newDisplayRect.right - newDisplayRect.left,
newDisplayRect.bottom - newDisplayRect.top,
SetWindowPosFlags.SWP_NOZORDER |
SetWindowPosFlags.SWP_NOACTIVATE);
return IntPtr.Zero;
}
case WindowsMessage.WM_KEYDOWN:
case WindowsMessage.WM_SYSKEYDOWN:
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
_owner,
RawKeyEventType.KeyDown,
KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_MENUCHAR:
{
// mute the system beep
return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16);
}
case WindowsMessage.WM_KEYUP:
case WindowsMessage.WM_SYSKEYUP:
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
_owner,
RawKeyEventType.KeyUp,
KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)),
WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_CHAR:
{
// Ignore control chars
if (ToInt32(wParam) >= 32)
{
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner,
new string((char)ToInt32(wParam), 1));
}
break;
}
case WindowsMessage.WM_LBUTTONDOWN:
case WindowsMessage.WM_RBUTTONDOWN:
case WindowsMessage.WM_MBUTTONDOWN:
case WindowsMessage.WM_XBUTTONDOWN:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
WindowsMessage.WM_XBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Down :
RawPointerEventType.XButton2Down
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_LBUTTONUP:
case WindowsMessage.WM_RBUTTONUP:
case WindowsMessage.WM_MBUTTONUP:
case WindowsMessage.WM_XBUTTONUP:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp,
WindowsMessage.WM_XBUTTONUP =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Up :
RawPointerEventType.XButton2Up,
},
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEMOVE:
{
if (ShouldIgnoreTouchEmulatedMessage())
{
break;
}
if (!_trackingMouse)
{
var tm = new TRACKMOUSEEVENT
{
cbSize = Marshal.SizeOf<TRACKMOUSEEVENT>(),
dwFlags = 2,
hwndTrack = _hwnd,
dwHoverTime = 0,
};
TrackMouseEvent(ref tm);
}
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.Move,
DipFromLParam(lParam), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEWHEEL:
{
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSEHWHEEL:
{
e = new RawMouseWheelEventArgs(
_mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_MOUSELEAVE:
{
_trackingMouse = false;
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
RawPointerEventType.LeaveWindow,
new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers);
break;
}
case WindowsMessage.WM_NCLBUTTONDOWN:
case WindowsMessage.WM_NCRBUTTONDOWN:
case WindowsMessage.WM_NCMBUTTONDOWN:
case WindowsMessage.WM_NCXBUTTONDOWN:
{
e = new RawPointerEventArgs(
_mouseDevice,
timestamp,
_owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType
.NonClientLeftButtonDown,
WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown,
WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown,
WindowsMessage.WM_NCXBUTTONDOWN =>
HighWord(ToInt32(wParam)) == 1 ?
RawPointerEventType.XButton1Down :
RawPointerEventType.XButton2Down,
},
PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam));
break;
}
case WindowsMessage.WM_TOUCH:
{
var touchInputCount = wParam.ToInt32();
var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
var touchInputs = new Span<TOUCHINPUT>(pTouchInputs, touchInputCount);
if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf<TOUCHINPUT>()))
{
foreach (var touchInput in touchInputs)
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id));
}
CloseTouchInputHandle(lParam);
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCPAINT:
{
if (!HasFullDecorations)
{
return IntPtr.Zero;
}
break;
}
case WindowsMessage.WM_NCACTIVATE:
{
if (!HasFullDecorations)
{
return new IntPtr(1);
}
break;
}
case WindowsMessage.WM_PAINT:
{
using (_rendererLock.Lock())
{
if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero)
{
var f = Scaling;
var r = ps.rcPaint;
Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f,
(r.bottom - r.top) / f));
EndPaint(_hwnd, ref ps);
}
}
return IntPtr.Zero;
}
case WindowsMessage.WM_SIZE:
{
using (_rendererLock.Lock())
{
// Do nothing here, just block until the pending frame render is completed on the render thread
}
var size = (SizeCommand)wParam;
if (Resized != null &&
(size == SizeCommand.Restored ||
size == SizeCommand.Maximized))
{
var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16);
Resized(clientSize / Scaling);
}
var windowState = size == SizeCommand.Maximized ?
WindowState.Maximized :
(size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal);
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowStateChanged?.Invoke(windowState);
}
return IntPtr.Zero;
}
case WindowsMessage.WM_MOVE:
{
PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff),
(short)(ToInt32(lParam) >> 16)));
return IntPtr.Zero;
}
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
if (_minSize.Width > 0)
{
mmi.ptMinTrackSize.X =
(int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right);
}
if (_minSize.Height > 0)
{
mmi.ptMinTrackSize.Y =
(int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom);
}
if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0)
{
mmi.ptMaxTrackSize.X =
(int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right);
}
if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0)
{
mmi.ptMaxTrackSize.Y =
(int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom);
}
Marshal.StructureToPtr(mmi, lParam, true);
return IntPtr.Zero;
}
case WindowsMessage.WM_DISPLAYCHANGE:
{
(Screen as ScreenImpl)?.InvalidateScreensCache();
return IntPtr.Zero;
}
}
#if USE_MANAGED_DRAG
if (_managedDrag.PreprocessInputEvent(ref e))
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
#endif
if (e != null && Input != null)
{
Input(e);
if (e.Handled)
{
return IntPtr.Zero;
}
}
using (_rendererLock.Lock())
{
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
private static int ToInt32(IntPtr ptr)
{
if (IntPtr.Size == 4)
return ptr.ToInt32();
return (int)(ptr.ToInt64() & 0xffffffff);
}
private static int HighWord(int param) => param >> 16;
private Point DipFromLParam(IntPtr lParam)
{
return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling;
}
private PixelPoint PointFromLParam(IntPtr lParam)
{
return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16));
}
private bool ShouldIgnoreTouchEmulatedMessage()
{
if (!_multitouch)
{
return false;
}
// MI_WP_SIGNATURE
// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
const long marker = 0xFF515700L;
var info = GetMessageExtraInfo().ToInt64();
return (info & marker) == marker;
}
private static RawInputModifiers GetMouseModifiers(IntPtr wParam)
{
var keys = (ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON))
{
modifiers |= RawInputModifiers.LeftMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON))
{
modifiers |= RawInputModifiers.RightMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON))
{
modifiers |= RawInputModifiers.MiddleMouseButton;
}
if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1))
if (_isClientAreaExtended)
{
modifiers |= RawInputModifiers.XButton1MouseButton;
lRet = CustomCaptionProc(hWnd, msg, wParam, lParam, ref callDwp);
}
if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2))
if (callDwp)
{
modifiers |= RawInputModifiers.XButton2MouseButton;
lRet = AppWndProc(hWnd, msg, wParam, lParam);
}
return modifiers;
return lRet;
}
}
}

160
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -40,7 +40,10 @@ namespace Avalonia.Win32
private SavedWindowInfo _savedWindowInfo;
private bool _isFullScreenActive;
private bool _isClientAreaExtended;
private Thickness _extendedMargins;
private Thickness _offScreenMargin;
#if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag;
#endif
@ -66,7 +69,7 @@ namespace Avalonia.Win32
private OleDropTarget _dropTarget;
private Size _minSize;
private Size _maxSize;
private WindowImpl _parent;
private WindowImpl _parent;
public WindowImpl()
{
@ -185,6 +188,11 @@ namespace Avalonia.Win32
{
get
{
if(_isFullScreenActive)
{
return WindowState.FullScreen;
}
var placement = default(WINDOWPLACEMENT);
GetWindowPlacement(_hwnd, ref placement);
@ -668,6 +676,96 @@ namespace Avalonia.Win32
}
TaskBarList.MarkFullscreen(_hwnd, fullscreen);
ExtendClientArea();
}
private MARGINS UpdateExtendMargins()
{
RECT borderThickness = new RECT();
RECT borderCaptionThickness = new RECT();
AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0);
AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0);
borderThickness.left *= -1;
borderThickness.top *= -1;
borderCaptionThickness.left *= -1;
borderCaptionThickness.top *= -1;
bool wantsTitleBar = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) || _extendTitleBarHint == -1;
if (!wantsTitleBar)
{
borderCaptionThickness.top = 1;
}
MARGINS margins = new MARGINS();
margins.cxLeftWidth = 1;
margins.cxRightWidth = 1;
margins.cyBottomHeight = 1;
if (_extendTitleBarHint != -1)
{
borderCaptionThickness.top = (int)(_extendTitleBarHint * Scaling);
}
margins.cyTopHeight = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemTitleBar) ? borderCaptionThickness.top : 1;
if (WindowState == WindowState.Maximized)
{
_extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / Scaling, 0, 0);
_offScreenMargin = new Thickness(borderThickness.left / Scaling, borderThickness.top / Scaling, borderThickness.right / Scaling, borderThickness.bottom / Scaling);
}
else
{
_extendedMargins = new Thickness(0, (borderCaptionThickness.top) / Scaling, 0, 0);
_offScreenMargin = new Thickness();
}
return margins;
}
private void ExtendClientArea ()
{
if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled)
{
_isClientAreaExtended = false;
return;
}
if (!_isClientAreaExtended || WindowState == WindowState.FullScreen)
{
_extendedMargins = new Thickness(0, 0, 0, 0);
_offScreenMargin = new Thickness();
}
else
{
GetWindowRect(_hwnd, out var rcClient);
// Inform the application of the frame change.
SetWindowPos(_hwnd,
IntPtr.Zero,
rcClient.left, rcClient.top,
rcClient.Width, rcClient.Height,
SetWindowPosFlags.SWP_FRAMECHANGED);
if (_isClientAreaExtended)
{
var margins = UpdateExtendMargins();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
}
else
{
var margins = new MARGINS();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
_offScreenMargin = new Thickness();
_extendedMargins = new Thickness();
}
}
ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended);
}
private void ShowWindow(WindowState state)
@ -818,9 +916,10 @@ namespace Avalonia.Win32
// Otherwise it will still show in the taskbar.
}
WindowStyles style;
if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges)
{
var style = GetStyle();
style = GetStyle();
if (newProperties.IsResizable)
{
@ -841,7 +940,7 @@ namespace Avalonia.Win32
if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges)
{
var style = GetStyle();
style = GetStyle();
const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU;
@ -886,7 +985,26 @@ namespace Avalonia.Win32
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
SetWindowPosFlags.SWP_FRAMECHANGED);
}
}
}
}
private const int MF_BYCOMMAND = 0x0;
private const int MF_BYPOSITION = 0x400;
private const int MF_REMOVE = 0x1000;
private const int MF_ENABLED = 0x0;
private const int MF_GRAYED = 0x1;
private const int MF_DISABLED = 0x2;
private const int SC_CLOSE = 0xF060;
void DisableCloseButton(IntPtr hwnd)
{
EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE,
MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
}
void EnableCloseButton(IntPtr hwnd)
{
EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE,
MF_BYCOMMAND | MF_ENABLED);
}
#if USE_MANAGED_DRAG
@ -912,6 +1030,38 @@ namespace Avalonia.Win32
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle;
public void SetExtendClientAreaToDecorationsHint(bool hint)
{
_isClientAreaExtended = hint;
ExtendClientArea();
}
private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default;
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
{
_extendChromeHints = hints;
ExtendClientArea();
}
private double _extendTitleBarHint = -1;
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
{
_extendTitleBarHint = titleBarHeight;
ExtendClientArea();
}
public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended;
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Thickness ExtendedMargins => _extendedMargins;
public Thickness OffScreenMargin => _offScreenMargin;
private struct SavedWindowInfo
{
public WindowStyles Style { get; set; }

16
tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs

@ -0,0 +1,16 @@
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Visuals
{
[MemoryDiagnoser, InProcess]
public class MatrixBenchmarks
{
private static readonly Matrix s_data = Matrix.Identity;
[Benchmark(Baseline = true)]
public bool Decompose()
{
return Matrix.TryDecomposeTransform(s_data, out Matrix.Decomposed decomposed);
}
}
}

89
tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs

@ -1,4 +1,5 @@
using System.Globalization;
using System;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
@ -6,11 +7,93 @@ namespace Avalonia.Visuals.UnitTests.Media
public class MatrixTests
{
[Fact]
public void Parse_Parses()
public void Can_Parse()
{
var matrix = Matrix.Parse("1,2,3,-4,5 6");
var expected = new Matrix(1, 2, 3, -4, 5, 6);
Assert.Equal(expected, matrix);
}
[Fact]
public void Singular_Has_No_Inverse()
{
var matrix = new Matrix(0, 0, 0, 0, 0, 0);
Assert.False(matrix.HasInverse);
}
[Fact]
public void Identity_Has_Inverse()
{
var matrix = Matrix.Identity;
Assert.True(matrix.HasInverse);
}
[Fact]
public void Can_Decompose_Translation()
{
var matrix = Matrix.CreateTranslation(5, 10);
var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed);
Assert.Equal(true, result);
Assert.Equal(5, decomposed.Translate.X);
Assert.Equal(10, decomposed.Translate.Y);
}
[Theory]
[InlineData(30d)]
[InlineData(0d)]
[InlineData(90d)]
[InlineData(270d)]
public void Can_Decompose_Angle(double angleDeg)
{
var angleRad = MathUtilities.Deg2Rad(angleDeg);
var matrix = Matrix.CreateRotation(angleRad);
var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed);
Assert.Equal(true, result);
var expected = NormalizeAngle(angleRad);
var actual = NormalizeAngle(decomposed.Angle);
Assert.Equal(expected, actual, 4);
}
[Theory]
[InlineData(1d, 1d)]
[InlineData(-1d, 1d)]
[InlineData(1d, -1d)]
[InlineData(5d, 10d)]
public void Can_Decompose_Scale(double x, double y)
{
var matrix = Matrix.CreateScale(x, y);
var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed);
Assert.Equal(true, result);
Assert.Equal(x, decomposed.Scale.X);
Assert.Equal(y, decomposed.Scale.Y);
}
private static double NormalizeAngle(double rad)
{
double twoPi = 2 * Math.PI;
while (rad < 0)
{
rad += twoPi;
}
while (rad > twoPi)
{
rad -= twoPi;
}
return rad;
}
}
}
}

133
tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs

@ -0,0 +1,133 @@
using Avalonia.Media.Transformation;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class TransformOperationsTests
{
[Fact]
public void Can_Parse_Compound_Operations()
{
var data = "scale(1,2) translate(3px,4px) rotate(5deg) skew(6deg,7deg)";
var transform = TransformOperations.Parse(data);
var operations = transform.Operations;
Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type);
Assert.Equal(1, operations[0].Data.Scale.X);
Assert.Equal(2, operations[0].Data.Scale.Y);
Assert.Equal(TransformOperation.OperationType.Translate, operations[1].Type);
Assert.Equal(3, operations[1].Data.Translate.X);
Assert.Equal(4, operations[1].Data.Translate.Y);
Assert.Equal(TransformOperation.OperationType.Rotate, operations[2].Type);
Assert.Equal(MathUtilities.Deg2Rad(5), operations[2].Data.Rotate.Angle);
Assert.Equal(TransformOperation.OperationType.Skew, operations[3].Type);
Assert.Equal(MathUtilities.Deg2Rad(6), operations[3].Data.Skew.X);
Assert.Equal(MathUtilities.Deg2Rad(7), operations[3].Data.Skew.Y);
}
[Fact]
public void Can_Parse_Matrix_Operation()
{
var data = "matrix(1,2,3,4,5,6)";
var transform = TransformOperations.Parse(data);
}
[Theory]
[InlineData(0d, 10d, 0d)]
[InlineData(0.5d, 5d, 10d)]
[InlineData(1d, 0d, 20d)]
public void Can_Interpolate_Translation(double progress, double x, double y)
{
var from = TransformOperations.Parse("translateX(10px)");
var to = TransformOperations.Parse("translateY(20px)");
var interpolated = TransformOperations.Interpolate(from, to, progress);
var operations = interpolated.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type);
Assert.Equal(x, operations[0].Data.Translate.X);
Assert.Equal(y, operations[0].Data.Translate.Y);
}
[Theory]
[InlineData(0d, 10d, 0d)]
[InlineData(0.5d, 5d, 10d)]
[InlineData(1d, 0d, 20d)]
public void Can_Interpolate_Scale(double progress, double x, double y)
{
var from = TransformOperations.Parse("scaleX(10)");
var to = TransformOperations.Parse("scaleY(20)");
var interpolated = TransformOperations.Interpolate(from, to, progress);
var operations = interpolated.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type);
Assert.Equal(x, operations[0].Data.Scale.X);
Assert.Equal(y, operations[0].Data.Scale.Y);
}
[Theory]
[InlineData(0d, 10d, 0d)]
[InlineData(0.5d, 5d, 10d)]
[InlineData(1d, 0d, 20d)]
public void Can_Interpolate_Skew(double progress, double x, double y)
{
var from = TransformOperations.Parse("skewX(10deg)");
var to = TransformOperations.Parse("skewY(20deg)");
var interpolated = TransformOperations.Interpolate(from, to, progress);
var operations = interpolated.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type);
Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X);
Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y);
}
[Theory]
[InlineData(0d, 10d)]
[InlineData(0.5d, 15d)]
[InlineData(1d,20d)]
public void Can_Interpolate_Rotation(double progress, double angle)
{
var from = TransformOperations.Parse("rotate(10deg)");
var to = TransformOperations.Parse("rotate(20deg)");
var interpolated = TransformOperations.Interpolate(from, to, progress);
var operations = interpolated.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type);
Assert.Equal(MathUtilities.Deg2Rad(angle), operations[0].Data.Rotate.Angle);
}
[Fact]
public void Interpolation_Fallback_To_Matrix()
{
double progress = 0.5d;
var from = TransformOperations.Parse("rotate(45deg)");
var to = TransformOperations.Parse("translate(100px, 100px) rotate(1215deg)");
var interpolated = TransformOperations.Interpolate(from, to, progress);
var operations = interpolated.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type);
}
}
}
Loading…
Cancel
Save