55 changed files with 3446 additions and 577 deletions
@ -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> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public interface IMutableTransform : ITransform |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Raised when the transform changes.
|
||||
|
/// </summary>
|
||||
|
event EventHandler Changed; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System.ComponentModel; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
[TypeConverter(typeof(TransformConverter))] |
||||
|
public interface ITransform |
||||
|
{ |
||||
|
Matrix Value { get; } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue