Browse Source

Merge branch 'master' into fixes/scrollchanged-behavior

pull/4080/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
fd08b9d343
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      readme.md
  2. 6
      samples/ControlCatalog/Models/Person.cs
  3. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  4. 15
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  5. 3
      samples/RenderDemo/MainWindow.xaml
  6. 101
      samples/RenderDemo/Pages/TransitionsPage.xaml
  7. 37
      samples/RenderDemo/Pages/TransitionsPage.xaml.cs
  8. 30
      src/Avalonia.Base/Utilities/MathUtilities.cs
  9. 64
      src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
  10. 6
      src/Avalonia.Controls/LayoutTransformControl.cs
  11. 5
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  12. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  13. 15
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  14. 32
      src/Avalonia.Controls/ScrollViewer.cs
  15. 3
      src/Avalonia.Controls/TopLevel.cs
  16. 95
      src/Avalonia.Controls/Window.cs
  17. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  18. 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  19. 5
      src/Avalonia.Layout/ILayoutRoot.cs
  20. 2
      src/Avalonia.Native/WindowImplBase.cs
  21. 2
      src/Avalonia.Themes.Default/ContextMenu.xaml
  22. 2
      src/Avalonia.Themes.Default/MenuItem.xaml
  23. 150
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  24. 10
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  25. 35
      src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs
  26. 28
      src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
  27. 75
      src/Avalonia.Visuals/Matrix.cs
  28. 128
      src/Avalonia.Visuals/Media/Color.cs
  29. 12
      src/Avalonia.Visuals/Media/IMutableTransform.cs
  30. 10
      src/Avalonia.Visuals/Media/ITransform.cs
  31. 5
      src/Avalonia.Visuals/Media/Transform.cs
  32. 23
      src/Avalonia.Visuals/Media/TransformConverter.cs
  33. 40
      src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs
  34. 230
      src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs
  35. 258
      src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs
  36. 444
      src/Avalonia.Visuals/Media/Transformation/TransformParser.cs
  37. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  38. 13
      src/Avalonia.Visuals/Size.cs
  39. 52
      src/Avalonia.Visuals/Vector.cs
  40. 14
      src/Avalonia.Visuals/Visual.cs
  41. 2
      src/Avalonia.Visuals/VisualTree/IVisual.cs
  42. 2
      src/Avalonia.X11/X11Window.cs
  43. 26
      src/Windows/Avalonia.Win32/PopupImpl.cs
  44. 2
      src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
  45. 14
      src/Windows/Avalonia.Win32/WindowImpl.cs
  46. 16
      tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs
  47. 20
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  48. 15
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  49. 2
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs
  50. 112
      tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs
  51. 89
      tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
  52. 229
      tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs

2
readme.md

@ -2,7 +2,7 @@
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
<img src="https://user-images.githubusercontent.com/6759207/84750966-ac74ad80-afc4-11ea-97a4-067d99c15a7d.png" width="500"/>
<img alt="Avalonia" src="https://user-images.githubusercontent.com/6759207/84897744-cab6d800-b0ae-11ea-8214-e5174d71f5c8.png" width="400"/>
## 📖 About AvaloniaUI

6
samples/ControlCatalog/Models/Person.cs

@ -21,12 +21,12 @@ namespace ControlCatalog.Models
get => _firstName;
set
{
_firstName = value;
if (string.IsNullOrWhiteSpace(value))
SetError(nameof(FirstName), "First Name Required");
else
SetError(nameof(FirstName), null);
_firstName = value;
OnPropertyChanged(nameof(FirstName));
}
@ -37,12 +37,12 @@ namespace ControlCatalog.Models
get => _lastName;
set
{
_lastName = value;
if (string.IsNullOrWhiteSpace(value))
SetError(nameof(LastName), "Last Name Required");
else
SetError(nameof(LastName), null);
_lastName = value;
OnPropertyChanged(nameof(LastName));
}
}
@ -95,4 +95,4 @@ namespace ControlCatalog.Models
return null;
}
}
}
}

2
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -11,5 +11,7 @@
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
<Button Name="OwnedWindow">Owned window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No taskbar icon)</Button>
</StackPanel>
</UserControl>

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

@ -93,6 +93,21 @@ namespace ControlCatalog.Pages
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
};
this.FindControl<Button>("OwnedWindow").Click += delegate
{
var window = CreateSampleWindow();
window.Show(GetWindow());
};
this.FindControl<Button>("OwnedWindowNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.ShowInTaskbar = false;
window.Show(GetWindow());
};
}
private Window CreateSampleWindow()

3
samples/RenderDemo/MainWindow.xaml

@ -29,6 +29,9 @@
<TabItem Header="Animations">
<pages:AnimationsPage/>
</TabItem>
<TabItem Header="Transitions">
<pages:TransitionsPage/>
</TabItem>
<TabItem Header="Clipping">
<pages:ClippingPage/>
</TabItem>

101
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -0,0 +1,101 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage">
<UserControl.Styles>
<Styles>
<Styles.Resources>
<Template x:Key="Acorn">
<Path Fill="White" Stretch="Uniform"
Data="F1 M 16.6309,18.6563C 17.1309,
8.15625 29.8809,14.1563 29.8809,
14.1563C 30.8809,11.1563 34.1308,
11.4063 34.1308,11.4063C 33.5,12
34.6309,13.1563 34.6309,13.1563C
32.1309,13.1562 31.1309,14.9062
31.1309,14.9062C 41.1309,23.9062
32.6309,27.9063 32.6309,27.9062C
24.6309,24.9063 21.1309,22.1562
16.6309,18.6563 Z M 16.6309,19.9063C
21.6309,24.1563 25.1309,26.1562
31.6309,28.6562C 31.6309,28.6562
26.3809,39.1562 18.3809,36.1563C
18.3809,36.1563 18,38 16.3809,36.9063C
15,36 16.3809,34.9063 16.3809,34.9063C
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z"/>
</Template>
<Template x:Key="Heart">
<Path Fill="Red" Stretch="Uniform" Data="
M 272.70141,238.71731
C 206.46141,238.71731 152.70146,292.4773 152.70146,358.71731
C 152.70146,493.47282 288.63461,528.80461 381.26391,662.02535
C 468.83815,529.62199 609.82641,489.17075 609.82641,358.71731
C 609.82641,292.47731 556.06651,238.7173 489.82641,238.71731
C 441.77851,238.71731 400.42481,267.08774 381.26391,307.90481
C 362.10311,267.08773 320.74941,238.7173 272.70141,238.71731 z "/>
</Template>
</Styles.Resources>
<Style Selector="Border.Test">
<Setter Property="Margin" Value="15"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="100"/>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:1" />
</Transitions>
</Setter>
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="Border.Rect1:pointerover">
<Setter Property="RenderTransform" Value="rotate(120deg) scale(1.5)" />
</Style>
<Style Selector="Border.Rect2:pointerover">
<Setter Property="RenderTransform" Value="scale(0.8)" />
</Style>
<Style Selector="Border.Rect3">
<Setter Property="Child" Value="{StaticResource Heart}"/>
</Style>
<Style Selector="Border.Rect3:pointerover">
<Setter Property="RenderTransform" Value="rotate(1turn)" />
</Style>
<Style Selector="Border.Rect4:pointerover">
<Setter Property="RenderTransform" Value="translateY(-100px)" />
</Style>
<Style Selector="Border.Rect5:pointerover">
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
<Style Selector="Border.Rect5:pointerover">
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
</Styles>
</UserControl.Styles>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/>
<Border Classes="Test Rect2" Background="Magenta"/>
<Border Classes="Test Rect3"/>
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>

37
samples/RenderDemo/Pages/TransitionsPage.xaml.cs

@ -0,0 +1,37 @@
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using RenderDemo.ViewModels;
namespace RenderDemo.Pages
{
public class TransitionsPage : UserControl
{
public TransitionsPage()
{
InitializeComponent();
this.DataContext = new AnimationsPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ToggleClock(object sender, RoutedEventArgs args)
{
var button = sender as Button;
var clock = button.Clock;
if (clock.PlayState == PlayState.Run)
{
clock.PlayState = PlayState.Pause;
}
else if (clock.PlayState == PlayState.Pause)
{
clock.PlayState = PlayState.Run;
}
}
}
}

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

@ -267,6 +267,36 @@ namespace Avalonia.Utilities
}
}
/// <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;
}
private static void ThrowCannotBeGreaterThanException(double min, double max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");

64
src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Controls.Primitives;
using Avalonia.Data.Converters;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters
{
public class MenuScrollingVisibilityConverter : IMultiValueConverter
{
public static readonly MenuScrollingVisibilityConverter Instance = new MenuScrollingVisibilityConverter();
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null ||
values == null ||
values.Count != 4 ||
!(values[0] is ScrollBarVisibility visiblity) ||
!(values[1] is double offset) ||
!(values[2] is double extent) ||
!(values[3] is double viewport))
{
return AvaloniaProperty.UnsetValue;
}
if (visiblity == ScrollBarVisibility.Auto)
{
if (extent == viewport)
{
return false;
}
double target;
if (parameter is double d)
{
target = d;
}
else if (parameter is string s)
{
target = double.Parse(s, NumberFormatInfo.InvariantInfo);
}
else
{
return AvaloniaProperty.UnsetValue;
}
// Calculate the percent so that we can see if we are near the edge of the range
double percent = MathUtilities.Clamp(offset * 100.0 / (extent - viewport), 0, 100);
if (MathUtilities.AreClose(percent, target))
{
// We are at the end of the range, so no need for this button to be shown
return false;
}
return true;
}
return false;
}
}
}

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

5
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Controls;
namespace Avalonia.Platform
{
@ -46,9 +45,9 @@ namespace Avalonia.Platform
IPlatformHandle Handle { get; }
/// <summary>
/// Gets the maximum size of a window on the system.
/// Gets a maximum client size hint for an auto-sizing window, in device-independent pixels.
/// </summary>
Size MaxClientSize { get; }
Size MaxAutoSizeHint { get; }
/// <summary>
/// Sets whether this window appears on top of all other windows

2
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -113,7 +113,7 @@ namespace Avalonia.Controls.Presenters
{
var scrollable = (ILogicalScrollable)Owner;
var visualRoot = Owner.GetVisualRoot();
var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxClientSize
var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxAutoSizeHint
?? (visualRoot as TopLevel)?.ClientSize;
// If infinity is passed as the available size and we're virtualized then we need to

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

@ -121,7 +121,20 @@ namespace Avalonia.Controls.Primitives
protected override Size MeasureOverride(Size availableSize)
{
var measured = base.MeasureOverride(availableSize);
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
var constraint = availableSize;
if (double.IsInfinity(constraint.Width))
{
constraint = constraint.WithWidth(maxAutoSize.Width);
}
if (double.IsInfinity(constraint.Height))
{
constraint = constraint.WithHeight(maxAutoSize.Height);
}
var measured = base.MeasureOverride(constraint);
var width = measured.Width;
var height = measured.Height;
var widthCache = Width;

32
src/Avalonia.Controls/ScrollViewer.cs

@ -385,6 +385,38 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary>
/// Scrolls the content up one line.
/// </summary>
public void LineUp()
{
Offset -= new Vector(0, _smallChange.Height);
}
/// <summary>
/// Scrolls the content down one line.
/// </summary>
public void LineDown()
{
Offset += new Vector(0, _smallChange.Height);
}
/// <summary>
/// Scrolls the content left one line.
/// </summary>
public void LineLeft()
{
Offset -= new Vector(_smallChange.Width, 0);
}
/// <summary>
/// Scrolls the content right one line.
/// </summary>
public void LineRight()
{
Offset += new Vector(_smallChange.Width, 0);
}
/// <summary>
/// Scrolls to the top-left corner of the content.
/// </summary>

3
src/Avalonia.Controls/TopLevel.cs

@ -276,9 +276,6 @@ namespace Avalonia.Controls
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => Size.Infinity;
/// <inheritdoc/>
double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1;

95
src/Avalonia.Controls/Window.cs

@ -69,7 +69,7 @@ namespace Avalonia.Controls
/// </summary>
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot
{
private List<Window> _children = new List<Window>();
private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>();
/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
@ -188,7 +188,7 @@ namespace Avalonia.Controls
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
@ -318,9 +318,6 @@ namespace Avalonia.Controls
/// </summary>
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
/// <inheritdoc/>
Type IStyleable.StyleKey => typeof(Window);
@ -376,7 +373,7 @@ namespace Avalonia.Controls
private void CloseInternal()
{
foreach (var child in _children.ToList())
foreach (var (child, _) in _children.ToList())
{
// if we HandleClosing() before then there will be no children.
child.CloseInternal();
@ -399,7 +396,7 @@ namespace Avalonia.Controls
{
bool canClose = true;
foreach (var child in _children.ToList())
foreach (var (child, _) in _children.ToList())
{
if (!child.HandleClosing())
{
@ -472,6 +469,28 @@ namespace Avalonia.Controls
/// The window has already been closed.
/// </exception>
public override void Show()
{
ShowCore(null);
}
/// <summary>
/// Shows the window as a child of <paramref name="parent"/>.
/// </summary>
/// <param name="parent">Window that will be a parent of the shown window.</param>
/// <exception cref="InvalidOperationException">
/// The window has already been closed.
/// </exception>
public void Show(Window parent)
{
if (parent is null)
{
throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent.");
}
ShowCore(parent);
}
private void ShowCore(Window parent)
{
if (PlatformImpl == null)
{
@ -483,7 +502,7 @@ namespace Avalonia.Controls
return;
}
this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized();
IsVisible = true;
@ -504,6 +523,14 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
if (parent != null)
{
PlatformImpl?.SetParent(parent.PlatformImpl);
}
Owner = parent;
parent?.AddChild(this, false);
PlatformImpl?.Show();
Renderer?.Start();
}
@ -571,9 +598,9 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
PlatformImpl.SetParent(owner.PlatformImpl);
PlatformImpl?.SetParent(owner.PlatformImpl);
Owner = owner;
owner.AddChild(this);
owner.AddChild(this, true);
PlatformImpl?.Show();
Renderer?.Start();
@ -598,28 +625,57 @@ namespace Avalonia.Controls
private void UpdateEnabled()
{
PlatformImpl.SetEnabled(_children.Count == 0);
bool isEnabled = true;
foreach (var (_, isDialog) in _children)
{
if (isDialog)
{
isEnabled = false;
break;
}
}
PlatformImpl.SetEnabled(isEnabled);
}
private void AddChild(Window window)
private void AddChild(Window window, bool isDialog)
{
_children.Add(window);
_children.Add((window, isDialog));
UpdateEnabled();
}
private void RemoveChild(Window window)
{
_children.Remove(window);
for (int i = _children.Count - 1; i >= 0; i--)
{
var (child, _) = _children[i];
if (ReferenceEquals(child, window))
{
_children.RemoveAt(i);
}
}
UpdateEnabled();
}
private void OnGotInputWhenDisabled()
{
var firstChild = _children.FirstOrDefault();
Window firstDialogChild = null;
foreach (var (child, isDialog) in _children)
{
if (isDialog)
{
firstDialogChild = child;
break;
}
}
if (firstChild != null)
if (firstDialogChild != null)
{
firstChild.OnGotInputWhenDisabled();
firstDialogChild.OnGotInputWhenDisabled();
}
else
{
@ -663,15 +719,16 @@ namespace Avalonia.Controls
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
var constraint = clientSize;
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{
constraint = constraint.WithWidth(double.PositiveInfinity);
constraint = constraint.WithWidth(maxAutoSize.Width);
}
if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{
constraint = constraint.WithHeight(double.PositiveInfinity);
constraint = constraint.WithHeight(maxAutoSize.Height);
}
var result = base.MeasureOverride(constraint);

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

@ -44,7 +44,7 @@ namespace Avalonia.DesignerSupport.Remote
public IPlatformHandle Handle { get; }
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public Size MaxClientSize { get; } = new Size(4096, 4096);
public Size MaxAutoSizeHint { get; } = new Size(4096, 4096);
public event Action LostFocus
{
add {}

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

@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action Deactivated { get; set; }
public Action Activated { get; set; }
public IPlatformHandle Handle { get; }
public Size MaxClientSize { get; }
public Size MaxAutoSizeHint { get; }
public Size ClientSize { get; }
public double Scaling { get; } = 1.0;
public IEnumerable<object> Surfaces { get; }

5
src/Avalonia.Layout/ILayoutRoot.cs

@ -10,11 +10,6 @@ namespace Avalonia.Layout
/// </summary>
Size ClientSize { get; }
/// <summary>
/// The maximum client size available.
/// </summary>
Size MaxClientSize { get; }
/// <summary>
/// The scaling factor to use in layout.
/// </summary>

2
src/Avalonia.Native/WindowImplBase.cs

@ -336,7 +336,7 @@ namespace Avalonia.Native
_native.BeginMoveDrag();
}
public Size MaxClientSize => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
public void SetTopmost(bool value)

2
src/Avalonia.Themes.Default/ContextMenu.xaml

@ -10,7 +10,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ScrollViewer>
<ScrollViewer Classes="menuscroller">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

2
src/Avalonia.Themes.Default/MenuItem.xaml

@ -113,7 +113,7 @@
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer>
<ScrollViewer Classes="menuscroller">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

150
src/Avalonia.Themes.Default/ScrollViewer.xaml

@ -1,47 +1,105 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ScrollViewer">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<ScrollContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls">
<Style Selector="ScrollViewer">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<ScrollContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Grid.Row="1"
Focusable="False"/>
<ScrollBar Name="verticalScrollBar"
Orientation="Vertical"
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Grid.Column="1"
Focusable="False"/>
<Panel Grid.Row="1" Grid.Column="1" Background="{DynamicResource ThemeControlMidBrush}"/>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Grid.Row="1"
Focusable="False"/>
<ScrollBar Name="verticalScrollBar"
Orientation="Vertical"
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Grid.Column="1"
Focusable="False"/>
<Panel Grid.Row="1" Grid.Column="1" Background="{DynamicResource ThemeControlMidBrush}"/>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ScrollViewer.menuscroller">
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<RepeatButton DockPanel.Dock="Top"
BorderThickness="0"
Background="Transparent"
Command="{Binding LineUp, RelativeSource={RelativeSource TemplatedParent}}">
<RepeatButton.IsVisible>
<MultiBinding Converter="{x:Static converters:MenuScrollingVisibilityConverter.Instance}"
ConverterParameter="0">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="VerticalScrollBarVisibility"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Offset.Y"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Extent.Height"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Viewport.Height"/>
</MultiBinding>
</RepeatButton.IsVisible>
<Path Data="M 0 4 L 8 4 L 4 0 Z"/>
</RepeatButton>
<RepeatButton DockPanel.Dock="Bottom"
BorderThickness="0"
Background="Transparent"
Command="{Binding LineDown, RelativeSource={RelativeSource TemplatedParent}}">
<RepeatButton.IsVisible>
<MultiBinding Converter="{x:Static converters:MenuScrollingVisibilityConverter.Instance}"
ConverterParameter="100">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="VerticalScrollBarVisibility"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Offset.Y"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Extent.Height"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Viewport.Height"/>
</MultiBinding>
</RepeatButton.IsVisible>
<Path Data="M 0 0 L 4 4 L 8 0 Z"/>
</RepeatButton>
<ScrollContentPresenter Name="PART_ContentPresenter"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/>
</DockPanel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ScrollViewer.menuscroller /template/ RepeatButton > Path">
<Setter Property="Fill" Value="{DynamicResource ThemeForegroundLowBrush}" />
</Style>
<Style Selector="ScrollViewer.menuscroller /template/ RepeatButton:pointerover > Path">
<Setter Property="Fill" Value="{DynamicResource ThemeAccentBrush}" />
</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<TransformOperations>
{
public TransformOperationsAnimator()
{
Validate = ValidateTransform;
}
public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue)
{
var oldTransform = EnsureOperations(oldValue);
var newTransform = EnsureOperations(newValue);
return TransformOperations.Interpolate(oldTransform, newTransform, progress);
}
internal static TransformOperations EnsureOperations(ITransform value)
{
return value as TransformOperations ?? TransformOperations.Identity;
}
private void ValidateTransform(AnimatorKeyFrame kf)
{
if (!(kf.Value is TransformOperations))
{
throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}.");
}
}
}
}

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

@ -0,0 +1,28 @@
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)
{
var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue);
var newTransform = TransformOperationsAnimator.EnsureOperations(newValue);
return progress
.Select(p =>
{
var f = Easing.Ease(p);
return _operationsAnimator.Interpolate(f, oldTransform, newTransform);
});
}
}
}

75
src/Avalonia.Visuals/Matrix.cs

@ -54,7 +54,7 @@ namespace Avalonia
/// <summary>
/// HasInverse Property - returns true if this matrix is invertible, false otherwise.
/// </summary>
public bool HasInverse => GetDeterminant() != 0;
public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
/// <summary>
/// The first element of the first row
@ -286,7 +286,7 @@ namespace Avalonia
{
double d = GetDeterminant();
if (d == 0)
if (MathUtilities.IsZero(d))
{
throw new InvalidOperationException("Transform is not invertible.");
}
@ -319,5 +319,76 @@ namespace Avalonia
);
}
}
/// <summary>
/// Decomposes given matrix into transform operations.
/// </summary>
/// <param name="matrix">Matrix to decompose.</param>
/// <param name="decomposed">Decomposed matrix.</param>
/// <returns>The status of the operation.</returns>
public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
{
decomposed = default;
var determinant = matrix.GetDeterminant();
if (MathUtilities.IsZero(determinant))
{
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;
}
}
}

128
src/Avalonia.Visuals/Media/Color.cs

@ -89,33 +89,64 @@ namespace Avalonia.Media
/// <returns>The <see cref="Color"/>.</returns>
public static Color Parse(string s)
{
if (s == null) throw new ArgumentNullException(nameof(s));
if (s.Length == 0) throw new FormatException();
if (TryParse(s, out Color color))
{
return color;
}
if (s[0] == '#')
throw new FormatException($"Invalid color string: '{s}'.");
}
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <returns>The <see cref="Color"/>.</returns>
public static Color Parse(ReadOnlySpan<char> s)
{
if (TryParse(s, out Color color))
{
var or = 0u;
return color;
}
if (s.Length == 7)
{
or = 0xff000000;
}
else if (s.Length != 9)
{
throw new FormatException($"Invalid color string: '{s}'.");
}
throw new FormatException($"Invalid color string: '{s.ToString()}'.");
}
return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or);
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <param name="color">The parsed color</param>
/// <returns>The status of the operation.</returns>
public static bool TryParse(string s, out Color color)
{
if (s == null)
{
throw new ArgumentNullException(nameof(s));
}
if (s.Length == 0)
{
throw new FormatException();
}
if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
{
return true;
}
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
return knownColor.ToColor();
color = knownColor.ToColor();
return true;
}
throw new FormatException($"Invalid color string: '{s}'.");
color = default;
return false;
}
/// <summary>
@ -126,40 +157,79 @@ namespace Avalonia.Media
/// <returns>The status of the operation.</returns>
public static bool TryParse(ReadOnlySpan<char> s, out Color color)
{
color = default;
if (s == null)
return false;
if (s.Length == 0)
{
color = default;
return false;
}
if (s[0] == '#')
{
var or = 0u;
return TryParseInternal(s, out color);
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
color = default;
return false;
}
private static bool TryParseInternal(ReadOnlySpan<char> s, out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
{
var alphaComponent = 0u;
if (s.Length == 7)
if (input.Length == 6)
{
or = 0xff000000;
alphaComponent = 0xff000000;
}
else if (s.Length != 9)
else if (input.Length != 8)
{
return false;
}
if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
// TODO: (netstandard 2.1) Can use allocation free parsing.
if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out var parsed))
{
return false;
color = FromUInt32(parsed| or);
}
color = FromUInt32(parsed | alphaComponent);
return true;
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
color = default;
if (knownColor != KnownColor.None)
ReadOnlySpan<char> input = s.Slice(1);
// Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB).
if (input.Length == 3 || input.Length == 4)
{
color = knownColor.ToColor();
return true;
var extendedLength = 2 * input.Length;
Span<char> extended = stackalloc char[extendedLength];
for (int i = 0; i < input.Length; i++)
{
extended[2 * i + 0] = input[i];
extended[2 * i + 1] = input[i];
}
return TryParseCore(extended, ref color);
}
return false;
return TryParseCore(input, ref color);
}
/// <summary>

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

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

@ -0,0 +1,230 @@
using System.Runtime.InteropServices;
namespace Avalonia.Media.Transformation
{
/// <summary>
/// Represents a single primitive transform (like translation, rotation, scale, etc.).
/// </summary>
public struct TransformOperation
{
public OperationType Type;
public Matrix Matrix;
public DataLayout Data;
public enum OperationType
{
Translate,
Rotate,
Scale,
Skew,
Matrix,
Identity
}
/// <summary>
/// Returns whether operation produces the identity matrix.
/// </summary>
public bool IsIdentity => Matrix.IsIdentity;
/// <summary>
/// Bakes this operation to a transform matrix.
/// </summary>
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;
}
}
}
/// <summary>
/// Returns new identity transform operation.
/// </summary>
public static TransformOperation Identity =>
new TransformOperation { Matrix = Matrix.Identity, Type = OperationType.Identity };
/// <summary>
/// Attempts to interpolate between two transform operations.
/// </summary>
/// <param name="from">Source operation.</param>
/// <param name="to">Target operation.</param>
/// <param name="progress">Interpolation progress.</param>
/// <param name="result">Interpolation result that will be filled in when operation was successful.</param>
/// <remarks>
/// Based upon https://www.w3.org/TR/css-transforms-1/#interpolation-of-transform-functions.
/// </remarks>
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;
}
// ReSharper disable PossibleInvalidOperationException
TransformOperation fromValue = fromIdentity ? Identity : from.Value;
TransformOperation toValue = toIdentity ? Identity : to.Value;
// ReSharper restore PossibleInvalidOperationException
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;
}
private static bool IsOperationIdentity(ref TransformOperation? operation)
{
return !operation.HasValue || operation.Value.IsIdentity;
}
[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;
}
}
}
}

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

@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Media.Transformation
{
/// <summary>
/// Contains a list of <see cref="TransformOperation"/> that represent primitive transforms that will be
/// applied in declared order.
/// </summary>
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();
}
/// <summary>
/// Returns whether all operations combined together produce the identity matrix.
/// </summary>
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);
}
}
}
}

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

@ -0,0 +1,444 @@
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.ScaleY)
{
scaleY = scaleX;
scaleX = UnitValue.One;
}
else if (function == TransformFunction.Scale && 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.SkewY)
{
skewY = skewX;
skewX = UnitValue.Zero;
}
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.TranslateY)
{
translateY = translateX;
translateX = UnitValue.Zero;
}
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.Gradian:
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")]

13
src/Avalonia.Visuals/Size.cs

@ -189,7 +189,7 @@ namespace Avalonia
}
/// <summary>
/// Returns a boolean indicating whether the size is equal to the other given size.
/// Returns a boolean indicating whether the size is equal to the other given size (bitwise).
/// </summary>
/// <param name="other">The other size to test equality against.</param>
/// <returns>True if this size is equal to other; False otherwise.</returns>
@ -201,6 +201,17 @@ namespace Avalonia
// ReSharper enable CompareOfFloatsByEqualityOperator
}
/// <summary>
/// Returns a boolean indicating whether the size is equal to the other given size (numerically).
/// </summary>
/// <param name="other">The other size to test equality against.</param>
/// <returns>True if this size is equal to other; False otherwise.</returns>
public bool NearlyEquals(Size other)
{
return MathUtilities.AreClose(_width, other._width) &&
MathUtilities.AreClose(_height, other._height);
}
/// <summary>
/// Checks for equality between a size and an object.
/// </summary>

52
src/Avalonia.Visuals/Vector.cs

@ -2,7 +2,8 @@ using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
using JetBrains.Annotations;
#nullable enable
namespace Avalonia
{
@ -17,20 +18,20 @@ namespace Avalonia
}
/// <summary>
/// The X vector.
/// The X component.
/// </summary>
private readonly double _x;
/// <summary>
/// The Y vector.
/// The Y component.
/// </summary>
private readonly double _y;
/// <summary>
/// Initializes a new instance of the <see cref="Vector"/> structure.
/// </summary>
/// <param name="x">The X vector.</param>
/// <param name="y">The Y vector.</param>
/// <param name="x">The X component.</param>
/// <param name="y">The Y component.</param>
public Vector(double x, double y)
{
_x = x;
@ -38,12 +39,12 @@ namespace Avalonia
}
/// <summary>
/// Gets the X vector.
/// Gets the X component.
/// </summary>
public double X => _x;
/// <summary>
/// Gets the Y vector.
/// Gets the Y component.
/// </summary>
public double Y => _y;
@ -57,18 +58,18 @@ namespace Avalonia
}
/// <summary>
/// Calculates the dot product of two vectors
/// Calculates the dot product of two vectors.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>The dot product</returns>
/// <param name="a">First vector.</param>
/// <param name="b">Second vector.</param>
/// <returns>The dot product.</returns>
public static double operator *(Vector a, Vector b)
=> Dot(a, b);
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator *(Vector vector, double scale)
@ -77,7 +78,7 @@ namespace Avalonia
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale)
@ -100,12 +101,12 @@ namespace Avalonia
}
/// <summary>
/// Length of the vector
/// Length of the vector.
/// </summary>
public double Length => Math.Sqrt(SquaredLength);
/// <summary>
/// Squared Length of the vector
/// Squared Length of the vector.
/// </summary>
public double SquaredLength => _x * _x + _y * _y;
@ -154,9 +155,8 @@ namespace Avalonia
/// <returns>True if vectors are nearly equal.</returns>
public bool NearlyEquals(Vector other)
{
const float tolerance = float.Epsilon;
return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
return MathUtilities.AreClose(_x, other._x) &&
MathUtilities.AreClose(_y, other._y);
}
public override bool Equals(object obj) => obj is Vector other && Equals(other);
@ -189,9 +189,9 @@ namespace Avalonia
}
/// <summary>
/// Returns a new vector with the specified X coordinate.
/// Returns a new vector with the specified X component.
/// </summary>
/// <param name="x">The X coordinate.</param>
/// <param name="x">The X component.</param>
/// <returns>The new vector.</returns>
public Vector WithX(double x)
{
@ -199,9 +199,9 @@ namespace Avalonia
}
/// <summary>
/// Returns a new vector with the specified Y coordinate.
/// Returns a new vector with the specified Y component.
/// </summary>
/// <param name="y">The Y coordinate.</param>
/// <param name="y">The Y component.</param>
/// <returns>The new vector.</returns>
public Vector WithY(double y)
{
@ -311,25 +311,25 @@ namespace Avalonia
=> new Vector(-vector._x, -vector._y);
/// <summary>
/// Returnes the vector (0.0, 0.0)
/// Returns the vector (0.0, 0.0).
/// </summary>
public static Vector Zero
=> new Vector(0, 0);
/// <summary>
/// Returnes the vector (1.0, 1.0)
/// Returns the vector (1.0, 1.0).
/// </summary>
public static Vector One
=> new Vector(1, 1);
/// <summary>
/// Returnes the vector (1.0, 0.0)
/// Returns the vector (1.0, 0.0).
/// </summary>
public static Vector UnitX
=> new Vector(1, 0);
/// <summary>
/// Returnes the vector (0.0, 1.0)
/// Returns the vector (0.0, 1.0).
/// </summary>
public static Vector UnitY
=> new Vector(0, 1);

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.

2
src/Avalonia.X11/X11Window.cs

@ -922,7 +922,7 @@ namespace Avalonia.X11
public IScreenImpl Screen => _platform.Screens;
public Size MaxClientSize => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
public Size MaxAutoSizeHint => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();

26
src/Windows/Avalonia.Win32/PopupImpl.cs

@ -8,12 +8,35 @@ namespace Avalonia.Win32
class PopupImpl : WindowImpl, IPopupImpl
{
private bool _dropShadowHint = true;
private Size? _maxAutoSize;
public override void Show()
{
UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate);
}
public override Size MaxAutoSizeHint
{
get
{
if (_maxAutoSize is null)
{
var monitor = UnmanagedMethods.MonitorFromWindow(
Hwnd,
UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero)
{
var info = UnmanagedMethods.MONITORINFO.Create();
UnmanagedMethods.GetMonitorInfo(monitor, ref info);
_maxAutoSize = info.rcWork.ToPixelRect().ToRect(Scaling).Size;
}
}
return _maxAutoSize ?? Size.Infinity;
}
}
protected override IntPtr CreateWindowOverride(ushort atom)
{
UnmanagedMethods.WindowStyles style =
@ -47,6 +70,9 @@ namespace Avalonia.Win32
{
switch ((UnmanagedMethods.WindowsMessage)msg)
{
case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE:
_maxAutoSize = null;
goto default;
case UnmanagedMethods.WindowsMessage.WM_MOUSEACTIVATE:
return (IntPtr)UnmanagedMethods.MouseActivate.MA_NOACTIVATE;
default:

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

@ -393,6 +393,8 @@ namespace Avalonia.Win32
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
_maxTrackSize = mmi.ptMaxTrackSize;
if (_minSize.Width > 0)
{

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

@ -66,6 +66,7 @@ namespace Avalonia.Win32
private OleDropTarget _dropTarget;
private Size _minSize;
private Size _maxSize;
private POINT _maxTrackSize;
private WindowImpl _parent;
public WindowImpl()
@ -168,16 +169,7 @@ namespace Avalonia.Win32
public IPlatformHandle Handle { get; private set; }
public Size MaxClientSize
{
get
{
return (new Size(
GetSystemMetrics(SystemMetric.SM_CXMAXTRACK),
GetSystemMetrics(SystemMetric.SM_CYMAXTRACK))
- BorderThickness) / Scaling;
}
}
public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling);
public IMouseDevice MouseDevice => _mouseDevice;
@ -211,6 +203,8 @@ namespace Avalonia.Win32
public WindowTransparencyLevel TransparencyLevel { get; private set; }
protected IntPtr Hwnd => _hwnd;
public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel)
{
TransparencyLevel = EnableBlur(transparencyLevel);

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

20
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@ -181,18 +182,21 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Child_Should_Be_Measured_With_Infinity()
public void Child_Should_Be_Measured_With_MaxAutoSizeHint()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000));
var target = CreateTarget(window, popupImpl.Object);
target.Content = child;
target.Show();
Assert.Equal(Size.Infinity, child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]);
}
}
@ -210,7 +214,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(500, 600), child.MeasureSizes[0]);
}
}
@ -228,7 +233,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(500, 600), child.MeasureSizes[0]);
}
}
@ -365,11 +371,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class ChildControl : Control
{
public Size MeasureSize { get; private set; }
public List<Size> MeasureSizes { get; } = new List<Size>();
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
MeasureSizes.Add(availableSize);
return base.MeasureOverride(availableSize);
}
}

15
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -297,12 +297,12 @@ namespace Avalonia.Controls.UnitTests
{
var parentWindowImpl = MockWindowingPlatform.CreateWindowMock();
parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
parentWindowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080));
parentWindowImpl.Setup(x => x.Scaling).Returns(1);
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200));
windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080));
windowImpl.Setup(x => x.Scaling).Returns(1);
var parentWindowServices = TestServices.StyledWindow.With(
@ -381,12 +381,15 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
public void Child_Should_Be_Measured_With_MaxAutoSizeHint_If_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000));
var child = new ChildControl();
var target = new Window
var target = new Window(windowImpl.Object)
{
Width = 100,
Height = 50,
@ -394,10 +397,10 @@ namespace Avalonia.Controls.UnitTests
Content = child
};
Show(target);
target.Show();
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]);
}
}

2
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -28,6 +28,7 @@ namespace Avalonia.UnitTests
windowImpl.SetupAllProperties();
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
windowImpl.Setup(x => x.Position).Returns(() => position);
@ -79,6 +80,7 @@ namespace Avalonia.UnitTests
popupImpl.SetupAllProperties();
popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
popupImpl.Setup(x => x.Scaling).Returns(1);
popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);

112
tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs

@ -17,6 +17,41 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
[Fact]
public void Try_Parse_Parses_RGB_Hash_Color()
{
var success = Color.TryParse("#ff8844", out Color result);
Assert.True(success);
Assert.Equal(0xff, result.R);
Assert.Equal(0x88, result.G);
Assert.Equal(0x44, result.B);
Assert.Equal(0xff, result.A);
}
[Fact]
public void Parse_Parses_RGB_Hash_Shorthand_Color()
{
var result = Color.Parse("#f84");
Assert.Equal(0xff, result.R);
Assert.Equal(0x88, result.G);
Assert.Equal(0x44, result.B);
Assert.Equal(0xff, result.A);
}
[Fact]
public void Try_Parse_Parses_RGB_Hash_Shorthand_Color()
{
var success = Color.TryParse("#f84", out Color result);
Assert.True(success);
Assert.Equal(0xff, result.R);
Assert.Equal(0x88, result.G);
Assert.Equal(0x44, result.B);
Assert.Equal(0xff, result.A);
}
[Fact]
public void Parse_Parses_ARGB_Hash_Color()
{
@ -28,6 +63,41 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0x40, result.A);
}
[Fact]
public void Try_Parse_Parses_ARGB_Hash_Color()
{
var success = Color.TryParse("#40ff8844", out Color result);
Assert.True(success);
Assert.Equal(0xff, result.R);
Assert.Equal(0x88, result.G);
Assert.Equal(0x44, result.B);
Assert.Equal(0x40, result.A);
}
[Fact]
public void Parse_Parses_ARGB_Hash_Shorthand_Color()
{
var result = Color.Parse("#4f84");
Assert.Equal(0xff, result.R);
Assert.Equal(0x88, result.G);
Assert.Equal(0x44, result.B);
Assert.Equal(0x44, result.A);
}
[Fact]
public void Try_Parse_Parses_ARGB_Hash_Shorthand_Color()
{
var success = Color.TryParse("#4f84", out Color result);
Assert.True(success);
Assert.Equal(0xff, result.R);
Assert.Equal(0x88, result.G);
Assert.Equal(0x44, result.B);
Assert.Equal(0x44, result.A);
}
[Fact]
public void Parse_Parses_Named_Color_Lowercase()
{
@ -39,6 +109,18 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
[Fact]
public void TryParse_Parses_Named_Color_Lowercase()
{
var success = Color.TryParse("red", out Color result);
Assert.True(success);
Assert.Equal(0xff, result.R);
Assert.Equal(0x00, result.G);
Assert.Equal(0x00, result.B);
Assert.Equal(0xff, result.A);
}
[Fact]
public void Parse_Parses_Named_Color_Uppercase()
{
@ -50,22 +132,52 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.A);
}
[Fact]
public void TryParse_Parses_Named_Color_Uppercase()
{
var success = Color.TryParse("RED", out Color result);
Assert.True(success);
Assert.Equal(0xff, result.R);
Assert.Equal(0x00, result.G);
Assert.Equal(0x00, result.B);
Assert.Equal(0xff, result.A);
}
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
{
Assert.Throws<FormatException>(() => Color.Parse("#ff"));
}
[Fact]
public void TryParse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
{
Assert.False(Color.TryParse("#ff", out _));
}
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Many_Chars()
{
Assert.Throws<FormatException>(() => Color.Parse("#ff5555555"));
}
[Fact]
public void TryParse_Hex_Value_Doesnt_Accept_Too_Many_Chars()
{
Assert.False(Color.TryParse("#ff5555555", out _));
}
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Invalid_Number()
{
Assert.Throws<FormatException>(() => Color.Parse("#ff808g80"));
}
[Fact]
public void TryParse_Hex_Value_Doesnt_Accept_Invalid_Number()
{
Assert.False(Color.TryParse("#ff808g80", out _));
}
}
}

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

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

@ -0,0 +1,229 @@
using Avalonia.Media.Transformation;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class TransformOperationsTests
{
[Theory]
[InlineData("translate(10px)", 10d, 0d)]
[InlineData("translate(10px, 10px)", 10d, 10d)]
[InlineData("translate(0px, 10px)", 0d, 10d)]
[InlineData("translate(10px, 0px)", 10d, 0d)]
[InlineData("translateX(10px)", 10d, 0d)]
[InlineData("translateY(10px)", 0d, 10d)]
public void Can_Parse_Translation(string data, double x, double y)
{
var transform = TransformOperations.Parse(data);
var operations = transform.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("rotate(90deg)", 90d)]
[InlineData("rotate(0.5turn)", 180d)]
[InlineData("rotate(200grad)", 180d)]
[InlineData("rotate(3.14159265rad)", 180d)]
public void Can_Parse_Rotation(string data, double angleDeg)
{
var transform = TransformOperations.Parse(data);
var operations = transform.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type);
Assert.Equal(MathUtilities.Deg2Rad(angleDeg), operations[0].Data.Rotate.Angle, 4);
}
[Theory]
[InlineData("scale(10)", 10d, 10d)]
[InlineData("scale(10, 10)", 10d, 10d)]
[InlineData("scale(0, 10)", 0d, 10d)]
[InlineData("scale(10, 0)", 10d, 0d)]
[InlineData("scaleX(10)", 10d, 1d)]
[InlineData("scaleY(10)", 1d, 10d)]
public void Can_Parse_Scale(string data, double x, double y)
{
var transform = TransformOperations.Parse(data);
var operations = transform.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("skew(90deg)", 90d, 0d)]
[InlineData("skew(0.5turn)", 180d, 0d)]
[InlineData("skew(200grad)", 180d, 0d)]
[InlineData("skew(3.14159265rad)", 180d, 0d)]
[InlineData("skewX(90deg)", 90d, 0d)]
[InlineData("skewX(0.5turn)", 180d, 0d)]
[InlineData("skewX(200grad)", 180d, 0d)]
[InlineData("skewX(3.14159265rad)", 180d, 0d)]
[InlineData("skew(0, 90deg)", 0d, 90d)]
[InlineData("skew(0, 0.5turn)", 0d, 180d)]
[InlineData("skew(0, 200grad)", 0d, 180d)]
[InlineData("skew(0, 3.14159265rad)", 0d, 180d)]
[InlineData("skewY(90deg)", 0d, 90d)]
[InlineData("skewY(0.5turn)", 0d, 180d)]
[InlineData("skewY(200grad)", 0d, 180d)]
[InlineData("skewY(3.14159265rad)", 0d, 180d)]
[InlineData("skew(90deg, 90deg)", 90d, 90d)]
[InlineData("skew(0.5turn, 0.5turn)", 180d, 180d)]
[InlineData("skew(200grad, 200grad)", 180d, 180d)]
[InlineData("skew(3.14159265rad, 3.14159265rad)", 180d, 180d)]
public void Can_Parse_Skew(string data, double x, double y)
{
var transform = TransformOperations.Parse(data);
var operations = transform.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type);
Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X, 4);
Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y, 4);
}
[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);
var operations = transform.Operations;
Assert.Single(operations);
Assert.Equal(TransformOperation.OperationType.Matrix, operations[0].Type);
var expectedMatrix = new Matrix(1, 2, 3, 4, 5, 6);
Assert.Equal(expectedMatrix, operations[0].Matrix);
}
[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, 1d)]
[InlineData(0.5d, 5.5d, 10.5d)]
[InlineData(1d, 1d, 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