From 95735d239b83c71e1c854827f1911e4aeccccd9f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 21 Apr 2020 16:17:52 +0200 Subject: [PATCH 01/55] Measure Window to MaxAutoSizeHint. - Renamed `MaxClientSize` to `MaxAutoSizeHint` - On Windows get its value from `WM_GETMINMAXINFO` message - Remove `ILayoutRoot.MaxClientSize` as it's not used any more --- src/Avalonia.Controls/Platform/IWindowBaseImpl.cs | 5 ++--- .../Presenters/ItemVirtualizerSimple.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 3 --- src/Avalonia.Controls/Window.cs | 10 ++++------ .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Layout/ILayoutRoot.cs | 5 ----- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.X11/X11Window.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs | 2 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 12 ++---------- tests/Avalonia.Controls.UnitTests/WindowTests.cs | 13 ++++++++----- tests/Avalonia.UnitTests/MockWindowingPlatform.cs | 1 + 13 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 8c99dffc28..b190c4f2e7 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/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; } /// - /// 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. /// - Size MaxClientSize { get; } + Size MaxAutoSizeHint { get; } /// /// Sets whether this window appears on top of all other windows diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b8d338741a..58fa8124db 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/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 diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index d17f3b0423..0be051d65b 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -212,9 +212,6 @@ namespace Avalonia.Controls set { SetValue(AccessText.ShowAccessKeyProperty, value); } } - /// - Size ILayoutRoot.MaxClientSize => Size.Infinity; - /// double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ee596432f7..f985a52467 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -184,7 +184,7 @@ namespace Avalonia.Controls { impl.Closing = HandleClosing; 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); @@ -314,9 +314,6 @@ namespace Avalonia.Controls /// public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e); - /// - Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; - /// Type IStyleable.StyleKey => typeof(Window); @@ -572,15 +569,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); diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 7480b3519c..57fcb785e0 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/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 WindowStateChanged { get; set; } - public Size MaxClientSize { get; } = new Size(4096, 4096); + public Size MaxAutoSizeHint { get; } = new Size(4096, 4096); public event Action LostFocus { add {} diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 7bf1d236bd..6d77d0dee5 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/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 Surfaces { get; } diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs index 56aca75871..e2f16b338a 100644 --- a/src/Avalonia.Layout/ILayoutRoot.cs +++ b/src/Avalonia.Layout/ILayoutRoot.cs @@ -10,11 +10,6 @@ namespace Avalonia.Layout /// Size ClientSize { get; } - /// - /// The maximum client size available. - /// - Size MaxClientSize { get; } - /// /// The scaling factor to use in layout. /// diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index c4ededd197..c97a3ebccb 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -309,7 +309,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) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 3a919e2bc4..340cad7842 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -915,7 +915,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(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 3ea8c1e48f..6c4ac3da2e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -399,6 +399,8 @@ namespace Avalonia.Win32 case WindowsMessage.WM_GETMINMAXINFO: { MINMAXINFO mmi = Marshal.PtrToStructure(lParam); + + _maxTrackSize = mmi.ptMaxTrackSize; if (_minSize.Width > 0) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e193c72ef7..aa61b0d48f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -62,6 +62,7 @@ namespace Avalonia.Win32 private OleDropTarget _dropTarget; private Size _minSize; private Size _maxSize; + private POINT _maxTrackSize; private WindowImpl _parent; public WindowImpl() @@ -164,16 +165,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 Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); public IMouseDevice MouseDevice => _mouseDevice; diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cf2920998a..d6634778ef 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/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( @@ -380,12 +380,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, @@ -396,7 +399,7 @@ namespace Avalonia.Controls.UnitTests target.Show(); Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(Size.Infinity, child.MeasureSizes[0]); + Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index b3e4b4edbc..105a028120 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/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); From 9391ddbab5285d0d555f714692bd00628c07db9b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 22 May 2020 10:33:58 +0200 Subject: [PATCH 02/55] Add failing tests for shorthand color parsing. --- .../Media/ColorTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs index e17fd47ff8..f8bd15593a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs @@ -17,6 +17,17 @@ namespace Avalonia.Visuals.UnitTests.Media 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 Parse_Parses_ARGB_Hash_Color() { @@ -28,6 +39,17 @@ namespace Avalonia.Visuals.UnitTests.Media 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 Parse_Parses_Named_Color_Lowercase() { From 34fa98c013cdb7b2c5b7b5b12078cee166bb593e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 22 May 2020 11:58:18 +0200 Subject: [PATCH 03/55] Unify color parsing and expose ROS and string based variants properly. --- src/Avalonia.Visuals/Media/Color.cs | 126 +++++++++++++++++++++------- 1 file changed, 97 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 2e06d2578f..d52642ff9c 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -89,33 +89,62 @@ namespace Avalonia.Media /// The . 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}'."); + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The . + public static Color Parse(ReadOnlySpan 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()}'."); + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The parsed color + /// The status of the operation. + public static bool TryParse(string s, out Color color) + { + if (s == null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (s.Length == 0) + { + throw new FormatException(); + } - return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); + if (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}'."); + return false; } /// @@ -126,40 +155,79 @@ namespace Avalonia.Media /// The status of the operation. public static bool TryParse(ReadOnlySpan 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; - if (s.Length == 7) + return false; + } + + private static bool TryParseInternal(ReadOnlySpan s, out Color color) + { + static bool TryParseCore(ReadOnlySpan input, ref Color color) + { + var alphaComponent = 0u; + + 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 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 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); } /// From 9c4bcbdea987b8a8b0a819cd78d2ba924973e91a Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 22 May 2020 11:58:35 +0200 Subject: [PATCH 04/55] Add more coverage for TryParse --- .../Media/ColorTests.cs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs index f8bd15593a..f3f3c9a4ca 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs @@ -17,6 +17,18 @@ 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() { @@ -28,6 +40,18 @@ namespace Avalonia.Visuals.UnitTests.Media 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() { @@ -39,6 +63,18 @@ 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() { @@ -50,6 +86,18 @@ namespace Avalonia.Visuals.UnitTests.Media 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() { @@ -61,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() { @@ -72,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(() => 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(() => 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(() => Color.Parse("#ff808g80")); } + + [Fact] + public void TryParse_Hex_Value_Doesnt_Accept_Invalid_Number() + { + Assert.False(Color.TryParse("#ff808g80", out _)); + } } } From cdce7d96348f27f15c758a299a521d522184719f Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 22 May 2020 13:24:47 +0200 Subject: [PATCH 05/55] Avoid redundant parsing of named colors in TryParse. --- src/Avalonia.Visuals/Media/Color.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index d52642ff9c..052ee5e1b7 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -130,7 +130,7 @@ namespace Avalonia.Media throw new FormatException(); } - if (TryParseInternal(s.AsSpan(), out color)) + if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color)) { return true; } @@ -144,6 +144,8 @@ namespace Avalonia.Media return true; } + color = default; + return false; } @@ -212,7 +214,7 @@ namespace Avalonia.Media ReadOnlySpan input = s.Slice(1); - // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB) + // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB). if (input.Length == 3 || input.Length == 4) { var extendedLength = 2 * input.Length; From d69718ab61d75c5542449a436bca5202b33e8b3e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 23 May 2020 21:00:41 +0200 Subject: [PATCH 06/55] Reimplement new child window support. --- samples/ControlCatalog/Pages/DialogsPage.xaml | 2 + .../ControlCatalog/Pages/DialogsPage.xaml.cs | 15 ++++ src/Avalonia.Controls/Window.cs | 84 ++++++++++++++++--- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 0834e829d8..a0e82663bf 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -11,5 +11,7 @@ + + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index dcb94a89e7..cf6c771e34 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -93,6 +93,21 @@ namespace ControlCatalog.Pages window.ShowInTaskbar = false; window.ShowDialog(GetWindow()); }; + this.FindControl public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { - private List _children = new List(); + private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>(); /// /// Defines the property. @@ -376,7 +376,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 +399,7 @@ namespace Avalonia.Controls { bool canClose = true; - foreach (var child in _children.ToList()) + foreach (var (child, _) in _children.ToList()) { if (!child.HandleClosing()) { @@ -472,6 +472,27 @@ namespace Avalonia.Controls /// The window has already been closed. /// public override void Show() + { + ShowCore(null); + } + + /// + /// Shows the window as a child of . + /// + /// + /// The window has already been closed. + /// + 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 +504,7 @@ namespace Avalonia.Controls return; } - this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); EnsureInitialized(); IsVisible = true; @@ -504,6 +525,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 +600,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 +627,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 { From a04e187fa09fd3255024e77183b43f484e101f40 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 23 May 2020 21:02:43 +0200 Subject: [PATCH 07/55] Extra parameter doc. --- src/Avalonia.Controls/Window.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 24e0e90417..782dc69b39 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -479,6 +479,7 @@ namespace Avalonia.Controls /// /// Shows the window as a child of . /// + /// Window that will be a parent of the shown window. /// /// The window has already been closed. /// From fd30ecc903d1fc98553b199d564493830c6c5b0c Mon Sep 17 00:00:00 2001 From: usUyGBx <64971385+usUyGBx@users.noreply.github.com> Date: Sun, 31 May 2020 21:40:32 +0300 Subject: [PATCH 08/55] Set property value before using any Interfaces --- samples/ControlCatalog/Models/Person.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Models/Person.cs b/samples/ControlCatalog/Models/Person.cs index 4248cb8056..a0abcfe8f4 100644 --- a/samples/ControlCatalog/Models/Person.cs +++ b/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; } } -} \ No newline at end of file +} From f39df90b93e1543d531058b058be292c85942ebc Mon Sep 17 00:00:00 2001 From: JamRemco <58340108+JamRemco@users.noreply.github.com> Date: Tue, 2 Jun 2020 16:41:28 +0200 Subject: [PATCH 09/55] Init Wip for ToggleSwitch --- src/Avalonia.Controls/ToggleSwitch.cs | 11 + .../Avalonia.Themes.Default.csproj | 8 + src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + src/Avalonia.Themes.Default/ToggleSwitch.xaml | 188 +++++++++++++++++ .../Accents/FluentControlResourcesLight.xaml | 3 + .../Avalonia.Themes.Fluent.csproj | 8 + src/Avalonia.Themes.Fluent/FluentTheme.xaml | 1 + src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 194 ++++++++++++++++++ 8 files changed, 414 insertions(+) create mode 100644 src/Avalonia.Controls/ToggleSwitch.cs create mode 100644 src/Avalonia.Themes.Default/ToggleSwitch.xaml create mode 100644 src/Avalonia.Themes.Fluent/ToggleSwitch.xaml diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs new file mode 100644 index 0000000000..bc9c6f1ff4 --- /dev/null +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// A check box control. + /// + public class ToggleSwitch : ToggleButton + { + } +} diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index c44cc358e8..d2c6d69bfe 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -17,6 +17,14 @@ + + + + + + MSBuild:Compile + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 67279fca99..3ed0115fff 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -52,4 +52,5 @@ + diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml new file mode 100644 index 0000000000..e929810376 --- /dev/null +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 15e157f573..15e0925f85 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -343,5 +343,8 @@ + + + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 3c3e14010d..00846f33fd 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -17,6 +17,14 @@ + + + + + + MSBuild:Compile + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 266acce971..dbbe3d6e69 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -52,4 +52,5 @@ + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml new file mode 100644 index 0000000000..1fe9788839 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 97bbb3ba92bf35ea80aaa0bf0868117147070466 Mon Sep 17 00:00:00 2001 From: JamRemco <58340108+JamRemco@users.noreply.github.com> Date: Wed, 3 Jun 2020 14:10:54 +0200 Subject: [PATCH 10/55] added light fluent resources --- src/Avalonia.Themes.Default/ToggleSwitch.xaml | 335 +++++++++--------- .../Accents/FluentControlResourcesDark.xaml | 62 ++++ src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 39 +- 3 files changed, 254 insertions(+), 182 deletions(-) diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index e929810376..3cdcb3f6c5 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -1,170 +1,177 @@ - + - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - + - + + + + + + + - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 7364c339f1..8265c22a88 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -343,5 +343,67 @@ + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 1fe9788839..2b665753b1 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -2,20 +2,21 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - + + + - + @@ -31,17 +32,18 @@ - - - - - + + --> + + From 23fc2c945ee542a7a12d3185f03683274e7b65a9 Mon Sep 17 00:00:00 2001 From: JamRemco <58340108+JamRemco@users.noreply.github.com> Date: Thu, 4 Jun 2020 20:33:32 +0200 Subject: [PATCH 11/55] Dark resources and some minors --- src/Avalonia.Controls/ToggleSwitch.cs | 7 +- src/Avalonia.Themes.Default/ToggleSwitch.xaml | 11 +- .../Accents/FluentControlResourcesDark.xaml | 114 +++++++++--------- .../Accents/FluentControlResourcesLight.xaml | 60 ++++++++- src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 12 +- 5 files changed, 134 insertions(+), 70 deletions(-) diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index bc9c6f1ff4..19f282d2d1 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -3,9 +3,14 @@ namespace Avalonia.Controls { /// - /// A check box control. + /// A WinUi like ToggleSwitch control. /// public class ToggleSwitch : ToggleButton { } } +/********** Todo *********** + * + * Implement ContenOff property + * Implement ContentOn property. +*/ diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index 3cdcb3f6c5..69ac64bb28 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -93,7 +93,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -126,7 +126,7 @@ - + --> + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 8265c22a88..da3c20e844 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -345,65 +345,65 @@ - + 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 15e0925f85..00e2e38ade 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -345,6 +345,64 @@ - + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 2b665753b1..061551f9e7 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -93,7 +93,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -119,14 +119,14 @@ - + - + - + --> diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 061551f9e7..b423416d7c 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -143,9 +143,9 @@ - + --> - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index b423416d7c..2c9309fe93 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -62,7 +62,7 @@ Name="PART_CircleThumb" Height="10" Width="10" Canvas.Top="7" - Canvas.Left="5"/> + /> @@ -126,7 +126,7 @@ - + + + + + + + + + + + + + + + + From 7b2ada673ac976b33015d83e799054d4bfcb18b1 Mon Sep 17 00:00:00 2001 From: JamRemco <58340108+JamRemco@users.noreply.github.com> Date: Wed, 10 Jun 2020 01:50:33 +0200 Subject: [PATCH 15/55] Fix Animation --- samples/ControlCatalog/App.xaml | 12 +- samples/ControlCatalog/MainView.xaml.cs | 44 +- samples/ControlCatalog/MainWindow.xaml | 2 +- .../Pages/AutoCompleteBoxPage.xaml | 1 + src/Avalonia.Themes.Default/ToggleSwitch.xaml | 141 +++++-- src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 396 ++++++++++-------- 6 files changed, 356 insertions(+), 240 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 20ca291910..de0c273dfb 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,10 +2,16 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.App"> - + + + + + + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 2d01d37dbe..3fd94bd60a 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -32,30 +32,30 @@ namespace ControlCatalog } - var light = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) - { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") - }; - var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) - { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") - }; + //var light = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + //{ + // Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") + //}; + //var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + //{ + // Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") + //}; - var themes = this.Find("Themes"); - themes.SelectionChanged += (sender, e) => - { - switch (themes.SelectedIndex) - { - case 0: - Styles[0] = light; - break; - case 1: - Styles[0] = dark; - break; - } - }; - Styles.Add(light); + //var themes = this.Find("Themes"); + //themes.SelectionChanged += (sender, e) => + //{ + // switch (themes.SelectedIndex) + // { + // case 0: + // Styles[0] = light; + // break; + // case 1: + // Styles[0] = dark; + // break; + // } + //}; + //Styles.Add(light); var decorations = this.Find("Decorations"); decorations.SelectionChanged += (sender, e) => diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a0bb956425..670e30725b 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent"> + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index f90a0c4658..8888cf921a 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -32,6 +32,7 @@ + Light diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index 556e32faab..f7e2c209a2 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -1,7 +1,9 @@ - + + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:sys="clr-namespace:System;assembly=netstandard"> + @@ -12,7 +14,7 @@ - @@ -44,9 +46,10 @@ - + + - - - + Canvas.Top="7"/> + - - + + + + - + + + - - + + + + + @@ -157,22 +182,45 @@ + - + + + + + - - - - - + + + + + + + + + + + + + + + + - - + + + + 5 + 28 + RoyalBlue + White - + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 2c9309fe93..bae8151563 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -1,177 +1,236 @@ + - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:sys="clr-namespace:System;assembly=netstandard"> + - + - - - + - + - - - + + /> - + + + + - - - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + 5 + 26 + RoyalBlue + + White + + + From fc711e7c86648a1c01b79977636b118c2cbdcfd6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 10 Jun 2020 05:48:07 +0200 Subject: [PATCH 16/55] Measure PopupRoot to MaxAutoSizeHint. On win32 popup's don't receive `WM_GETMINMAXINFO` so instead query the monitor info for the max work area, --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 15 ++++++++++- src/Windows/Avalonia.Win32/PopupImpl.cs | 26 +++++++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++- .../Primitives/PopupRootTests.cs | 20 +++++++++----- .../MockWindowingPlatform.cs | 1 + 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index aab7a68795..6a363d2a20 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -119,7 +119,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; diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index efcf1ea674..7f27a9e841 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/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: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e7eb623a5d..3afc962a76 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -169,7 +169,7 @@ namespace Avalonia.Win32 public IPlatformHandle Handle { get; private set; } - public Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); + public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); public IMouseDevice MouseDevice => _mouseDevice; @@ -203,6 +203,8 @@ namespace Avalonia.Win32 public WindowTransparencyLevel TransparencyLevel { get; private set; } + protected IntPtr Hwnd => _hwnd; + public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel) { TransparencyLevel = EnableBlur(transparencyLevel); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 08b9c75dbc..f27ff3928c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/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 MeasureSizes { get; } = new List(); protected override Size MeasureOverride(Size availableSize) { - MeasureSize = availableSize; + MeasureSizes.Add(availableSize); return base.MeasureOverride(availableSize); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 105a028120..ee45433089 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -80,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); From f7d0088d553593ab3079c7f0060a9664717e219b Mon Sep 17 00:00:00 2001 From: JamRemco <58340108+JamRemco@users.noreply.github.com> Date: Wed, 10 Jun 2020 18:46:54 +0200 Subject: [PATCH 17/55] Fix some colors --- samples/ControlCatalog/App.xaml | 11 +- src/Avalonia.Themes.Default/ToggleSwitch.xaml | 388 +++++++++-------- src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 404 +++++++++--------- 3 files changed, 421 insertions(+), 382 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index de0c273dfb..d289767100 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -2,14 +2,15 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.App"> - + - - + + + + + - - - - - - + - - + + - - - - - - - + + + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - + + + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - + + + + - - - - 5 - 28 - RoyalBlue - White - + + + 5 + 28 + RoyalBlue + + White + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index bae8151563..2f311043fd 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -1,8 +1,8 @@ - + - + @@ -14,8 +14,8 @@ Text="Updates will be automaticly Downloaded and installed shile the computer is shutting down or restarting" TextWrapping="Wrap"/> + FontWeight="Medium" + VerticalAlignment="Bottom" /> @@ -25,154 +25,147 @@ - + FontWeight="Medium"/> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + - - + + - - - - - - - + + + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - + + + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - + + + + + + + + 5 + 28 + RoyalBlue + White - - - 5 - 26 - RoyalBlue - - White - - + From 1ce6df71863841825651e83e48ea170ee64eb9c5 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Wed, 10 Jun 2020 19:56:00 +0300 Subject: [PATCH 18/55] Remove duplicate code for MathUtilities --- src/Avalonia.Base/Properties/AssemblyInfo.cs | 1 + src/Avalonia.Base/Utilities/MathUtilities.cs | 2 +- src/Avalonia.Controls.DataGrid/DataGrid.cs | 38 ++--- .../DataGridColumnHeader.cs | 2 +- .../DataGridColumns.cs | 16 +-- .../DataGridLength.cs | 2 +- src/Avalonia.Controls.DataGrid/DataGridRow.cs | 2 +- .../DataGridRows.cs | 60 ++++---- .../Primitives/DataGridCellsPresenter.cs | 6 +- .../Utils/DoubleUtil.cs | 136 ------------------ .../Utilities/MathUtilitiesTests.cs | 48 ++++++- 11 files changed, 110 insertions(+), 203 deletions(-) delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 0664f22dcb..692982cdc6 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -8,3 +8,4 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.UnitTests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")] diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 3fb5f7a162..6da695407b 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -9,7 +9,7 @@ namespace Avalonia.Utilities public static class MathUtilities { // smallest such that 1.0+DoubleEpsilon != 1.0 - private const double DoubleEpsilon = 2.2204460492503131e-016; + internal static readonly double DoubleEpsilon = 2.2204460492503131e-016; /// /// AreClose - Returns whether or not two doubles are "close". That is, whether or diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index cfe47a09d5..7e893d131e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2681,7 +2681,7 @@ namespace Avalonia.Controls { return; } - Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); + Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); _verticalScrollChangesIgnored++; try @@ -2698,7 +2698,7 @@ namespace Avalonia.Controls } else if (scrollEventType == ScrollEventType.SmallDecrement) { - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset; } @@ -2717,7 +2717,7 @@ namespace Avalonia.Controls DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset; } - if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) { // Invalidate so the scroll happens on idle InvalidateRowsMeasure(invalidateIndividualElements: false); @@ -3346,22 +3346,22 @@ namespace Avalonia.Controls bool needHorizScrollbarWithoutVertScrollbar = false; if (allowHorizScrollbar && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) && + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight)) { double oldDataHeight = cellsHeight; cellsHeight -= horizScrollBarHeight; Debug.Assert(cellsHeight >= 0); needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true; - if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || - DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) + if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || + MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) { // Would we still need a horizontal scrollbar without the vertical one? UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { - needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); + needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); } } @@ -3374,8 +3374,8 @@ namespace Avalonia.Controls UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); if (allowVertScrollbar && - DoubleUtil.GreaterThan(cellsHeight, 0) && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.GreaterThan(cellsHeight, 0) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3389,9 +3389,9 @@ namespace Avalonia.Controls if (allowHorizScrollbar && needVertScrollbar && !needHorizScrollbar && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) && + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight)) { cellsWidth += vertScrollBarWidth; cellsHeight -= horizScrollBarHeight; @@ -3422,7 +3422,7 @@ namespace Avalonia.Controls if (allowVertScrollbar) { if (cellsHeight > 0 && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3439,9 +3439,9 @@ namespace Avalonia.Controls if (allowHorizScrollbar) { if (cellsWidth > 0 && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth)) + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) && + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth)) { cellsHeight -= horizScrollBarHeight; Debug.Assert(cellsHeight >= 0); @@ -5387,7 +5387,7 @@ namespace Avalonia.Controls private void SetVerticalOffset(double newVerticalOffset) { _verticalOffset = newVerticalOffset; - if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value)) + if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value)) { _vScrollBar.Value = _verticalOffset; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index df2b03798a..e6cc7e5e40 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -301,7 +301,7 @@ namespace Avalonia.Controls private static bool CanResizeColumn(DataGridColumn column) { if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing && - (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) + (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !MathUtilities.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) { return false; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 16b63ad696..5b75bc73f9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated) { - if (!DoubleUtil.IsZero(amount)) + if (!MathUtilities.IsZero(amount)) { if (amount < 0) { @@ -777,7 +777,7 @@ namespace Avalonia.Controls private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated) { double remainingAdjustment = adjustment; - if (DoubleUtil.IsZero(remainingAdjustment)) + if (MathUtilities.IsZero(remainingAdjustment)) { return remainingAdjustment; } @@ -843,7 +843,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func targetWidth) { - if (DoubleUtil.IsZero(remainingAdjustment)) + if (MathUtilities.IsZero(remainingAdjustment)) { return remainingAdjustment; } @@ -1244,7 +1244,7 @@ namespace Avalonia.Controls Debug.Assert(amount < 0); Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star); - if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) + if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) { return amount; } @@ -1271,7 +1271,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double DecreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) { - if (DoubleUtil.GreaterThanOrClose(amount, 0)) + if (MathUtilities.GreaterThanOrClose(amount, 0)) { return amount; } @@ -1285,7 +1285,7 @@ namespace Avalonia.Controls (affectNewColumns || column.IsInitialDesiredWidthDetermined))) { amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount); - if (DoubleUtil.IsZero(amount)) + if (MathUtilities.IsZero(amount)) { break; } @@ -1392,7 +1392,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double IncreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) { - if (DoubleUtil.LessThanOrClose(amount, 0)) + if (MathUtilities.LessThanOrClose(amount, 0)) { return amount; } @@ -1406,7 +1406,7 @@ namespace Avalonia.Controls (affectNewColumns || column.IsInitialDesiredWidthDetermined))) { amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount); - if (DoubleUtil.IsZero(amount)) + if (MathUtilities.IsZero(amount)) { break; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridLength.cs b/src/Avalonia.Controls.DataGrid/DataGridLength.cs index 6a545a35ec..4841ddd494 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridLength.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridLength.cs @@ -529,7 +529,7 @@ namespace Avalonia.Controls // in this case drop value part and print only "Star" case DataGridLengthUnitType.Star: return ( - DoubleUtil.AreClose(1.0, dataGridLength.Value.Value) + MathUtilities.AreClose(1.0, dataGridLength.Value.Value) ? _starSuffix : Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index df200240ff..830eff1102 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -879,7 +879,7 @@ namespace Avalonia.Controls && (double.IsNaN(_detailsContent.Height)) && (AreDetailsVisible) && (!double.IsNaN(_detailsDesiredHeight)) - && !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) + && !MathUtilities.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) && Slot != -1) { _detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index fda14cf5b4..924156f5f4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -329,7 +329,7 @@ namespace Avalonia.Controls internal void OnRowsMeasure() { - if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) { ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight); DisplayData.PendingVerticalScrollHeight = 0; @@ -432,7 +432,7 @@ namespace Avalonia.Controls } else if (DisplayData.FirstScrollingSlot == slot && slot != -1) { - if (!DoubleUtil.IsZero(NegVerticalOffset)) + if (!MathUtilities.IsZero(NegVerticalOffset)) { // First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0. DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset; @@ -447,7 +447,7 @@ namespace Avalonia.Controls { // Scroll up to the new row so it becomes the first displayed row firstFullSlot = DisplayData.FirstScrollingSlot - 1; - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { deltaY = -NegVerticalOffset; } @@ -470,7 +470,7 @@ namespace Avalonia.Controls // Figure out how much of the last row is cut off double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot); double availableHeight = AvailableSlotElementRoom + rowHeight; - if (DoubleUtil.AreClose(rowHeight, availableHeight)) + if (MathUtilities.AreClose(rowHeight, availableHeight)) { if (DisplayData.LastScrollingSlot == slot) { @@ -499,7 +499,7 @@ namespace Avalonia.Controls { ResetDisplayedRows(); } - if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) + if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) { // The entire row won't fit in the DataGrid so we start showing it from the top NegVerticalOffset = 0; @@ -519,7 +519,7 @@ namespace Avalonia.Controls } // - Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset)); + Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset)); SetVerticalOffset(_verticalOffset); @@ -1660,7 +1660,7 @@ namespace Avalonia.Controls private void ScrollSlotsByHeight(double height) { Debug.Assert(DisplayData.FirstScrollingSlot >= 0); - Debug.Assert(!DoubleUtil.IsZero(height)); + Debug.Assert(!MathUtilities.IsZero(height)); _scrollingByHeight = true; try @@ -1672,7 +1672,7 @@ namespace Avalonia.Controls { // Scrolling Down int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount); - if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset)) + if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset)) { // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom // of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by @@ -1684,7 +1684,7 @@ namespace Avalonia.Controls else { deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset; - if (DoubleUtil.LessThan(height, deltaY)) + if (MathUtilities.LessThan(height, deltaY)) { // We've merely covered up more of the same row we're on NegVerticalOffset += height; @@ -1707,7 +1707,7 @@ namespace Avalonia.Controls } else { - while (DoubleUtil.LessThanOrClose(deltaY, height)) + while (MathUtilities.LessThanOrClose(deltaY, height)) { if (newFirstScrollingSlot < lastVisibleSlot) { @@ -1727,7 +1727,7 @@ namespace Avalonia.Controls double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double remainingHeight = height - deltaY; - if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight)) + if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight)) { deltaY += rowHeight; } @@ -1744,7 +1744,7 @@ namespace Avalonia.Controls else { // Scrolling Up - if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0)) + if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0)) { // We've merely exposing more of the row we're on NegVerticalOffset += height; @@ -1778,7 +1778,7 @@ namespace Avalonia.Controls else { int lastScrollingSlot = DisplayData.LastScrollingSlot; - while (DoubleUtil.GreaterThan(deltaY, height)) + while (MathUtilities.GreaterThan(deltaY, height)) { if (newFirstScrollingSlot > 0) { @@ -1797,7 +1797,7 @@ namespace Avalonia.Controls } double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double remainingHeight = height - deltaY; - if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0)) + if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0)) { deltaY -= rowHeight; } @@ -1809,7 +1809,7 @@ namespace Avalonia.Controls } } } - if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) + if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) { // We've scrolled to the top of the ScrollBar, automatically place the user at the very top // of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate. @@ -1822,7 +1822,7 @@ namespace Avalonia.Controls } double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); - if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset)) + if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) { // We've scrolled off more of the first row than what's possible. This can happen // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling @@ -1838,11 +1838,11 @@ namespace Avalonia.Controls UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight); double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot); - if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) + if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) { int firstElementSlot = DisplayData.FirstScrollingSlot; // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element - while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) + while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) { int previousSlot = GetPreviousVisibleSlot(firstElementSlot); if (previousSlot == -1) @@ -1872,7 +1872,7 @@ namespace Avalonia.Controls { _verticalOffset = NegVerticalOffset; } - else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset)) + else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset)) { // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb // can stay in the same place @@ -1890,8 +1890,8 @@ namespace Avalonia.Controls DisplayData.FullyRecycleElements(); - Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0)); - Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); + Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0)); + Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); } finally { @@ -2032,7 +2032,7 @@ namespace Avalonia.Controls double deltaY = -NegVerticalOffset; int visibleScrollingRows = 0; - if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) { return; } @@ -2044,7 +2044,7 @@ namespace Avalonia.Controls } int slot = firstDisplayedScrollingSlot; - while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight)) + while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight)) { deltaY += GetExactSlotElementHeight(slot); visibleScrollingRows++; @@ -2052,7 +2052,7 @@ namespace Avalonia.Controls slot = GetNextVisibleSlot(slot); } - while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) { slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot); if (slot >= 0) @@ -2063,14 +2063,14 @@ namespace Avalonia.Controls } } // If we're up to the first row, and we still have room left, uncover as much of the first row as we can - if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight)) + if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight)) { double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY); deltaY += NegVerticalOffset - newNegVerticalOffset; NegVerticalOffset = newNegVerticalOffset; } - if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0))) + if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && MathUtilities.GreaterThan(NegVerticalOffset, 0))) { DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1; } @@ -2108,7 +2108,7 @@ namespace Avalonia.Controls double deltaY = 0; int visibleScrollingRows = 0; - if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) { ResetDisplayedRows(); return; @@ -2120,7 +2120,7 @@ namespace Avalonia.Controls } int slot = lastDisplayedScrollingRow; - while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) { deltaY += GetExactSlotElementHeight(slot); visibleScrollingRows++; @@ -2542,7 +2542,7 @@ namespace Avalonia.Controls double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false); // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative // the user can still scroll to the top since the offset is non-zero - SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange)); + SetVerticalOffset(Math.Max(MathUtilities.DoubleEpsilon, _verticalOffset + heightChange)); } else { @@ -3024,4 +3024,4 @@ namespace Avalonia.Controls } #endif } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 0f513e7f42..6e0703c90f 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -310,9 +310,9 @@ namespace Avalonia.Controls.Primitives double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge; double rightEdge = leftEdge + column.ActualWidth; return - DoubleUtil.GreaterThan(rightEdge, 0) && - DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && - DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) + MathUtilities.GreaterThan(rightEdge, 0) && + MathUtilities.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && + MathUtilities.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) } } } diff --git a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs b/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs deleted file mode 100644 index ec0e8836d7..0000000000 --- a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs +++ /dev/null @@ -1,136 +0,0 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. -// All other rights reserved. - -using System; - -namespace Avalonia.Controls.Utils -{ - internal static class DoubleUtil - { - internal const double DBL_EPSILON = 1e-6; - - /// - /// AreClose - Returns whether or not two doubles are "close". That is, whether or - /// not they are within epsilon of each other. Note that this epsilon is proportional - /// to the numbers themselves to that AreClose survives scalar multiplication. - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the AreClose comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool AreClose(double value1, double value2) - { - //in case they are Infinities (then epsilon check does not work) - if (value1 == value2) return true; - // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON - double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON; - double delta = value1 - value2; - return (-eps < delta) && (eps > delta); - } - - /// - /// GreaterThan - Returns whether or not the first double is greater than the second double. - /// That is, whether or not the first is strictly greater than *and* not within epsilon of - /// the other number. Note that this epsilon is proportional to the numbers themselves - /// to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the GreaterThan comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool GreaterThan(double value1, double value2) - { - return (value1 > value2) && !AreClose(value1, value2); - } - - /// - /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to - /// the second double. That is, whether or not the first is strictly greater than or within - /// epsilon of the other number. Note that this epsilon is proportional to the numbers - /// themselves to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the GreaterThanOrClose comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool GreaterThanOrClose(double value1, double value2) - { - return (value1 > value2) || AreClose(value1, value2); - } - - /// - /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), - /// but this is faster. - /// - /// - /// bool - the result of the IsZero comparison. - /// - /// The double to compare to 0. - public static bool IsZero(double value) - { - return Math.Abs(value) < 10.0 * DBL_EPSILON; - } - - /// - /// LessThan - Returns whether or not the first double is less than the second double. - /// That is, whether or not the first is strictly less than *and* not within epsilon of - /// the other number. Note that this epsilon is proportional to the numbers themselves - /// to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the LessThan comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool LessThan(double value1, double value2) - { - return (value1 < value2) && !AreClose(value1, value2); - } - - /// - /// LessThanOrClose - Returns whether or not the first double is less than or close to - /// the second double. That is, whether or not the first is strictly less than or within - /// epsilon of the other number. Note that this epsilon is proportional to the numbers - /// themselves to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the LessThanOrClose comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool LessThanOrClose(double value1, double value2) - { - return (value1 < value2) || AreClose(value1, value2); - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index a36b22fee2..ccb5aef315 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -53,14 +53,14 @@ namespace Avalonia.Base.UnitTests.Utilities } [Fact] - public void Clamp_Input_NaN_Return_NaN() + public void Float_Clamp_Input_NaN_Return_NaN() { var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0); Assert.True(double.IsNaN(clamp)); } [Fact] - public void Clamp_Input_NegativeInfinity_Return_Min() + public void Float_Clamp_Input_NegativeInfinity_Return_Min() { const double min = 0.0; const double max = 1.0; @@ -69,12 +69,54 @@ namespace Avalonia.Base.UnitTests.Utilities } [Fact] - public void Clamp_Input_PositiveInfinity_Return_Max() + public void Float_Clamp_Input_PositiveInfinity_Return_Max() { const double min = 0.0; const double max = 1.0; var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max); Assert.Equal(max, actual); } + + [Fact] + public void Zero_Less_Than_One() + { + var actual = MathUtilities.LessThan(0, 1); + Assert.True(actual); + } + + [Fact] + public void One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1, 0); + Assert.False(actual); + } + + [Fact] + public void Zero_Not_Greater_Than_One() + { + var actual = MathUtilities.GreaterThan(0, 1); + Assert.False(actual); + } + + [Fact] + public void One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1, 0); + Assert.True(actual); + } + + [Fact] + public void One_Less_Than_Or_Close_One() + { + var actual = MathUtilities.LessThanOrClose(1, 1); + Assert.True(actual); + } + + [Fact] + public void One_Greater_Than_Or_Close_One() + { + var actual = MathUtilities.GreaterThanOrClose(1, 1); + Assert.True(actual); + } } } From 78993eb33cb062c0bba8cc352c17fae1ecd31484 Mon Sep 17 00:00:00 2001 From: JamRemco <58340108+JamRemco@users.noreply.github.com> Date: Fri, 12 Jun 2020 19:48:52 +0200 Subject: [PATCH 19/55] Add contentoff and contentOn to toggleswitch Add more resource suport --- src/Avalonia.Controls/ToggleSwitch.cs | 115 ++++- .../Accents/FluentControlResourcesDark.xaml | 7 +- .../Accents/FluentControlResourcesLight.xaml | 10 +- src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 465 ++++++++++-------- 4 files changed, 375 insertions(+), 222 deletions(-) diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs index 19f282d2d1..16caf8dbfb 100644 --- a/src/Avalonia.Controls/ToggleSwitch.cs +++ b/src/Avalonia.Controls/ToggleSwitch.cs @@ -1,16 +1,119 @@ using Avalonia.Controls.Primitives; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; + namespace Avalonia.Controls { /// /// A WinUi like ToggleSwitch control. /// + /// + public class ToggleSwitch : ToggleButton { - } + public static readonly StyledProperty OffContentProperty = + AvaloniaProperty.Register(nameof(OffContent)); + + public static readonly StyledProperty OffContentTemplateProperty = + AvaloniaProperty.Register(nameof(OffContentTemplate)); + + + public static readonly StyledProperty OnContentProperty = + AvaloniaProperty.Register(nameof(OnContent)); + + public static readonly StyledProperty OnContentTemplateProperty = + AvaloniaProperty.Register(nameof(OnContentTemplate)); + + public object OnContent + { + get { return GetValue(OnContentProperty); } + set { SetValue(OnContentProperty, value); } + } + + public object OffContent + { + get { return GetValue(OffContentProperty); } + set { SetValue(OffContentProperty, value); } + } + + public IContentPresenter OffContentPresenter + { + get; + private set; + } + + public IContentPresenter OnContentPresenter + { + get; + private set; + } + + + public IDataTemplate OffContentTemplate + { + get { return GetValue(OffContentTemplateProperty); } + set { SetValue(OffContentTemplateProperty, value); } + } + + public IDataTemplate OnContentTemplate + { + get { return GetValue(OnContentTemplateProperty); } + set { SetValue(OnContentTemplateProperty, value); } + } + + private void OffContentChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); + } + } + + private void OnContentChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); + } + } + + static ToggleSwitch() + { + OffContentProperty.Changed.AddClassHandler((x, e) => x.OffContentChanged(e)); + OnContentProperty.Changed.AddClassHandler((x, e) => x.OnContentChanged(e)); + } + + + protected override bool RegisterContentPresenter(IContentPresenter presenter) + { + var result = base.RegisterContentPresenter(presenter); + + if (presenter.Name == "Part_OnContentPresenter") + { + OnContentPresenter = presenter; + result = true; + } + if (presenter.Name == "PART_OffContentPresenter") + { + OffContentPresenter = presenter; + result = true; + } + + return result; + } + } } -/********** Todo *********** - * - * Implement ContenOff property - * Implement ContentOn property. -*/ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index da3c20e844..9fa0458db3 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -346,8 +346,10 @@ - 0 - 1 + + 0 + + 1 @@ -380,7 +382,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 2426c1034c..5c23bc96ee 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -345,8 +345,12 @@ - 0 - 1 + + + + 0 + + 1 @@ -379,7 +383,6 @@ - @@ -404,6 +407,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 2f311043fd..df9be1284a 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -4,7 +4,7 @@ xmlns:sys="clr-namespace:System;assembly=netstandard"> - + - @@ -26,244 +28,289 @@ Text="The previewer Shows a preview off your code, this could slow down your system" TextWrapping="Wrap"/> + /> - - + + + + + + + + - - + + + - - + + + + + + + + + + + + + - - + + + + + + + - - + - - - - - + - + + + + + + - - - - - - - + - - - - - - - + + + - - - - - - - - - - - - - - + - - - - - - - - + + + + + - + + + + + + + + 0,0,0,6 + 6 + 6 + 154 + 5 + 28 + + + From 0281678ae37899f136f6a0d422d39516c791a24b Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sat, 13 Jun 2020 15:33:02 +0300 Subject: [PATCH 20/55] Add check to bound in float Clamp --- src/Avalonia.Base/Utilities/MathUtilities.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 77c8eea0aa..8590c16a07 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -188,6 +188,11 @@ namespace Avalonia.Utilities /// The clamped value. public static double Clamp(double val, double min, double max) { + if (min > max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } + if (val < min) { return min; From 3307fd577b96ebc4baf962263bf81b984a0464f6 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sat, 13 Jun 2020 15:50:05 +0300 Subject: [PATCH 21/55] Replace check AreClose(x, 1) by IsOne in RoundLayoutValue --- src/Avalonia.Base/Utilities/MathUtilities.cs | 2 +- .../Utilities/MathUtilitiesTests.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 8590c16a07..6542bc8a89 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -221,7 +221,7 @@ namespace Avalonia.Utilities double newValue; // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.AreClose(dpiScale, 1.0)) + if (!MathUtilities.IsOne(dpiScale)) { newValue = Math.Round(value * dpiScale) / dpiScale; // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index 61f7a2a548..be1aa9ad2e 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -157,5 +157,24 @@ namespace Avalonia.Base.UnitTests.Utilities var actual = MathUtilities.GreaterThanOrClose(1, 1); Assert.True(actual); } + + [Fact] + public void Round_Layout_Value_Without_DPI_Aware() + { + const double value = 5e-15; + var expectedValue = Math.Round(value); + var actualValue = MathUtilities.RoundLayoutValue(value, 1.0); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void Round_Layout_Value_With_DPI_Aware() + { + const double dpiScale = 1.25; + const double value = 42.5; + var expectedValue = Math.Round(value * dpiScale) / dpiScale; + var actualValue = MathUtilities.RoundLayoutValue(value, dpiScale); + Assert.Equal(expectedValue, actualValue); + } } } From 725081e1871f30dd7f5da60320328428c879339c Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sat, 13 Jun 2020 15:57:40 +0300 Subject: [PATCH 22/55] Fix to not ~0 in test --- tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index be1aa9ad2e..2c35290986 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -161,7 +161,7 @@ namespace Avalonia.Base.UnitTests.Utilities [Fact] public void Round_Layout_Value_Without_DPI_Aware() { - const double value = 5e-15; + const double value = 42.5; var expectedValue = Math.Round(value); var actualValue = MathUtilities.RoundLayoutValue(value, 1.0); Assert.Equal(expectedValue, actualValue); From 5a65aa09f891e2c66c736a9122359f839e679325 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sat, 13 Jun 2020 17:57:08 +0300 Subject: [PATCH 23/55] Add some tests for double case MathUtilities --- .../Utilities/MathUtilitiesTests.cs | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index 2c35290986..a12d07b8ef 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -117,44 +117,86 @@ namespace Avalonia.Base.UnitTests.Utilities } [Fact] - public void Zero_Less_Than_One() + public void Double_Float_Zero_Less_Than_One() { - var actual = MathUtilities.LessThan(0, 1); + var actual = MathUtilities.LessThan(0d, 1d); Assert.True(actual); } [Fact] - public void One_Not_Less_Than_Zero() + public void Single_Float_Zero_Less_Than_One() { - var actual = MathUtilities.LessThan(1, 0); + var actual = MathUtilities.LessThan(0f, 1f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1d, 0d); + Assert.False(actual); + } + + [Fact] + public void Single_Float_One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1f, 0f); + Assert.False(actual); + } + + [Fact] + public void Double_Float_Zero_Not_Greater_Than_One() + { + var actual = MathUtilities.GreaterThan(0d, 1d); Assert.False(actual); } [Fact] - public void Zero_Not_Greater_Than_One() + public void Single_Float_Zero_Not_Greater_Than_One() { - var actual = MathUtilities.GreaterThan(0, 1); + var actual = MathUtilities.GreaterThan(0f, 1f); Assert.False(actual); } [Fact] - public void One_Greater_Than_Zero() + public void Double_Float_One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1d, 0d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1f, 0f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Less_Than_Or_Close_One() + { + var actual = MathUtilities.LessThanOrClose(1d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Less_Than_Or_Close_One() { - var actual = MathUtilities.GreaterThan(1, 0); + var actual = MathUtilities.LessThanOrClose(1f, 1f); Assert.True(actual); } [Fact] - public void One_Less_Than_Or_Close_One() + public void Double_Float_One_Greater_Than_Or_Close_One() { - var actual = MathUtilities.LessThanOrClose(1, 1); + var actual = MathUtilities.GreaterThanOrClose(1d, 1d); Assert.True(actual); } [Fact] - public void One_Greater_Than_Or_Close_One() + public void Single_Float_One_Greater_Than_Or_Close_One() { - var actual = MathUtilities.GreaterThanOrClose(1, 1); + var actual = MathUtilities.GreaterThanOrClose(1f, 1f); Assert.True(actual); } From 8c5b22c8cca7efa4f3631bf9e954bc9731ef5874 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 8 Jun 2020 23:01:54 +0200 Subject: [PATCH 24/55] Initial CSS like transform support. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 30 ++ .../LayoutTransformControl.cs | 6 +- .../Animation/Animators/TransformAnimator.cs | 10 +- .../Animators/TransformOperationsAnimator.cs | 35 ++ .../TransformOperationsTransition.cs | 25 + src/Avalonia.Visuals/Matrix.cs | 65 +++ .../Media/IMutableTransform.cs | 12 + src/Avalonia.Visuals/Media/ITransform.cs | 10 + src/Avalonia.Visuals/Media/Transform.cs | 5 +- .../Media/TransformConverter.cs | 23 + .../Transformation/InterpolationUtilities.cs | 40 ++ .../Transformation/TransformOperation.cs | 203 ++++++++ .../Transformation/TransformOperations.cs | 252 ++++++++++ .../Media/Transformation/TransformParser.cs | 463 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + src/Avalonia.Visuals/Visual.cs | 14 +- src/Avalonia.Visuals/VisualTree/IVisual.cs | 2 +- .../Media/MatrixTests.cs | 44 +- .../Media/TransformOperationsTests.cs | 128 +++++ 19 files changed, 1353 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs create mode 100644 src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs create mode 100644 src/Avalonia.Visuals/Media/IMutableTransform.cs create mode 100644 src/Avalonia.Visuals/Media/ITransform.cs create mode 100644 src/Avalonia.Visuals/Media/TransformConverter.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs create mode 100644 src/Avalonia.Visuals/Media/Transformation/TransformParser.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 7949a62949..fa5ab2d45b 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -262,5 +262,35 @@ namespace Avalonia.Utilities return val; } } + + /// + /// Converts an angle in degrees to radians. + /// + /// The angle in degrees. + /// The angle in radians. + public static double Deg2Rad(double angle) + { + return angle * (Math.PI / 180d); + } + + /// + /// Converts an angle in gradians to radians. + /// + /// The angle in gradians. + /// The angle in radians. + public static double Grad2Rad(double angle) + { + return angle * (Math.PI / 200d); + } + + /// + /// Converts an angle in turns to radians. + /// + /// The angle in turns. + /// The angle in radians. + public static double Turn2Rad(double angle) + { + return angle * 2 * Math.PI; + } } } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 8d48f6646d..83ad2b3638 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// public class LayoutTransformControl : Decorator { - public static readonly StyledProperty LayoutTransformProperty = - AvaloniaProperty.Register(nameof(LayoutTransform)); + public static readonly StyledProperty LayoutTransformProperty = + AvaloniaProperty.Register(nameof(LayoutTransform)); public static readonly StyledProperty UseRenderTransformProperty = AvaloniaProperty.Register(nameof(LayoutTransform)); @@ -37,7 +37,7 @@ namespace Avalonia.Controls /// /// Gets or sets a graphics transformation that should apply to this element when layout is performed. /// - public Transform LayoutTransform + public ITransform LayoutTransform { get { return GetValue(LayoutTransformProperty); } set { SetValue(LayoutTransformProperty, value); } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 1f1590bdcd..bb1c0da902 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/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)) diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs new file mode 100644 index 0000000000..f45338122f --- /dev/null +++ b/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 + { + 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; + } + } +} diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs new file mode 100644 index 0000000000..4911b34d91 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs @@ -0,0 +1,25 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Animation.Animators; +using Avalonia.Media; + +namespace Avalonia.Animation +{ + public class TransformOperationsTransition : Transition + { + private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); + + public override IObservable DoTransition(IObservable progress, + ITransform oldValue, + ITransform newValue) + { + return progress + .Select(p => + { + var f = Easing.Ease(p); + + return _operationsAnimator.Interpolate(f, oldValue, newValue); + }); + } + } +} diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 898c6027a5..e18140aa8d 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -319,5 +319,70 @@ namespace Avalonia ); } } + + public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) + { + decomposed = default; + + var determinant = matrix.GetDeterminant(); + + if (determinant == 0) + { + 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; + } } } diff --git a/src/Avalonia.Visuals/Media/IMutableTransform.cs b/src/Avalonia.Visuals/Media/IMutableTransform.cs new file mode 100644 index 0000000000..2033c434c0 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableTransform.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Media +{ + public interface IMutableTransform : ITransform + { + /// + /// Raised when the transform changes. + /// + event EventHandler Changed; + } +} diff --git a/src/Avalonia.Visuals/Media/ITransform.cs b/src/Avalonia.Visuals/Media/ITransform.cs new file mode 100644 index 0000000000..91577fe38e --- /dev/null +++ b/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; } + } +} diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 70ef1eaaf4..7cf1b35ada 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -8,11 +8,12 @@ namespace Avalonia.Media /// /// Represents a transform on an . /// - public abstract class Transform : Animatable + public abstract class Transform : Animatable, IMutableTransform { static Transform() { - Animation.Animation.RegisterAnimator(prop => typeof(Transform).IsAssignableFrom(prop.OwnerType)); + Animation.Animation.RegisterAnimator(prop => + typeof(ITransform).IsAssignableFrom(prop.OwnerType)); } /// diff --git a/src/Avalonia.Visuals/Media/TransformConverter.cs b/src/Avalonia.Visuals/Media/TransformConverter.cs new file mode 100644 index 0000000000..e79c0b8b7b --- /dev/null +++ b/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 +{ + /// + /// Creates an from a string representation. + /// + 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); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs new file mode 100644 index 0000000000..1e80eabfc8 --- /dev/null +++ b/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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs new file mode 100644 index 0000000000..cdf31f8e5b --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs @@ -0,0 +1,203 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Media.Transformation +{ + public struct TransformOperation + { + public OperationType Type; + public Matrix Matrix; + public DataLayout Data; + + public enum OperationType + { + Translate, + Rotate, + Scale, + Skew, + Matrix, + Identity + } + + public bool IsIdentity => Matrix.IsIdentity; + + public void Bake() + { + Matrix = Matrix.Identity; + + switch (Type) + { + case OperationType.Translate: + { + Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y); + + break; + } + case OperationType.Rotate: + { + Matrix = Matrix.CreateRotation(Data.Rotate.Angle); + + break; + } + case OperationType.Scale: + { + Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y); + + break; + } + case OperationType.Skew: + { + Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y); + + break; + } + } + } + + public static bool IsOperationIdentity(ref TransformOperation? operation) + { + return !operation.HasValue || operation.Value.IsIdentity; + } + + public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress, + ref TransformOperation result) + { + bool fromIdentity = IsOperationIdentity(ref from); + bool toIdentity = IsOperationIdentity(ref to); + + if (fromIdentity && toIdentity) + { + return true; + } + + TransformOperation fromValue = fromIdentity ? default : from.Value; + TransformOperation toValue = toIdentity ? default : to.Value; + + var interpolationType = toIdentity ? fromValue.Type : toValue.Type; + + result.Type = interpolationType; + + switch (interpolationType) + { + case OperationType.Translate: + { + double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X; + double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y; + + double toX = toIdentity ? 0 : toValue.Data.Translate.X; + double toY = toIdentity ? 0 : toValue.Data.Translate.Y; + + result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Rotate: + { + double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle; + + double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle; + + result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress); + + result.Bake(); + + break; + } + case OperationType.Scale: + { + double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X; + double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y; + + double toX = toIdentity ? 1 : toValue.Data.Scale.X; + double toY = toIdentity ? 1 : toValue.Data.Scale.Y; + + result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Skew: + { + double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X; + double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y; + + double toX = toIdentity ? 0 : toValue.Data.Skew.X; + double toY = toIdentity ? 0 : toValue.Data.Skew.Y; + + result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); + result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); + + result.Bake(); + + break; + } + case OperationType.Matrix: + { + var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix; + var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix; + + if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) || + !Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed)) + { + return false; + } + + var interpolated = + InterpolationUtilities.InterpolateDecomposedTransforms( + ref fromDecomposed, ref toDecomposed, + progress); + + result.Matrix = InterpolationUtilities.ComposeTransform(interpolated); + + break; + } + case OperationType.Identity: + { + // Do nothing. + break; + } + } + + return true; + } + + [StructLayout(LayoutKind.Explicit)] + public struct DataLayout + { + [FieldOffset(0)] public SkewLayout Skew; + + [FieldOffset(0)] public ScaleLayout Scale; + + [FieldOffset(0)] public TranslateLayout Translate; + + [FieldOffset(0)] public RotateLayout Rotate; + + public struct SkewLayout + { + public double X; + public double Y; + } + + public struct ScaleLayout + { + public double X; + public double Y; + } + + public struct TranslateLayout + { + public double X; + public double Y; + } + + public struct RotateLayout + { + public double Angle; + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs new file mode 100644 index 0000000000..9f711a2d63 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Avalonia.Media.Transformation +{ + public sealed class TransformOperations : ITransform + { + public static TransformOperations Identity { get; } = new TransformOperations(new List()); + + private readonly List _operations; + + private TransformOperations(List operations) + { + _operations = operations ?? throw new ArgumentNullException(nameof(operations)); + + IsIdentity = CheckIsIdentity(); + + Value = ApplyTransforms(); + } + + public bool IsIdentity { get; } + + public IReadOnlyList 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 _operations; + + public Builder(int capacity) + { + _operations = new List(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); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs new file mode 100644 index 0000000000..2a3912832b --- /dev/null +++ b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs @@ -0,0 +1,463 @@ +using System; +using System.Globalization; +using Avalonia.Utilities; + +namespace Avalonia.Media.Transformation +{ + public static class TransformParser + { + private static readonly (string, TransformFunction)[] s_functionMapping = + { + ("translate", TransformFunction.Translate), + ("translateX", TransformFunction.TranslateX), + ("translateY", TransformFunction.TranslateY), + ("scale", TransformFunction.Scale), + ("scaleX", TransformFunction.ScaleX), + ("scaleY", TransformFunction.ScaleY), + ("skew", TransformFunction.Skew), + ("skewX", TransformFunction.SkewX), + ("skewY", TransformFunction.SkewY), + ("rotate", TransformFunction.Rotate), + ("matrix", TransformFunction.Matrix) + }; + + private static readonly (string, Unit)[] s_unitMapping = + { + ("deg", Unit.Degree), + ("grad", Unit.Gradian), + ("rad", Unit.Radian), + ("turn", Unit.Turn), + ("px", Unit.Pixel) + }; + + public static TransformOperations Parse(string s) + { + void ThrowInvalidFormat() + { + throw new FormatException($"Invalid transform string: '{s}'."); + } + + if (string.IsNullOrEmpty(s)) + { + throw new ArgumentException(nameof(s)); + } + + var span = s.AsSpan().Trim(); + + if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return TransformOperations.Identity; + } + + var builder = TransformOperations.CreateBuilder(0); + + while (true) + { + var beginIndex = span.IndexOf('('); + var endIndex = span.IndexOf(')'); + + if (beginIndex == -1 || endIndex == -1) + { + ThrowInvalidFormat(); + } + + var namePart = span.Slice(0, beginIndex).Trim(); + + var function = ParseTransformFunction(in namePart); + + if (function == TransformFunction.Invalid) + { + ThrowInvalidFormat(); + } + + var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim(); + + ParseFunction(in valuePart, function, in builder); + + span = span.Slice(endIndex + 1); + + if (span.IsWhiteSpace()) + { + break; + } + } + + return builder.Build(); + } + + private static void ParseFunction( + in ReadOnlySpan functionPart, + TransformFunction function, + in TransformOperations.Builder builder) + { + static UnitValue ParseValue(ReadOnlySpan 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 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 part, in Span 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 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 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 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 + } + } +} diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 6cd6442095..5d802c27b9 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/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")] diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index bb9a4cf208..cd6e5bb075 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -68,8 +68,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty RenderTransformProperty = - AvaloniaProperty.Register(nameof(RenderTransform)); + public static readonly StyledProperty RenderTransformProperty = + AvaloniaProperty.Register(nameof(RenderTransform)); /// /// Defines the property. @@ -219,7 +219,7 @@ namespace Avalonia /// /// Gets the render transform of the control. /// - 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(); diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs index 6f905cc269..50787655d9 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs @@ -76,7 +76,7 @@ namespace Avalonia.VisualTree /// /// Gets or sets the render transform of the control. /// - Transform RenderTransform { get; set; } + ITransform RenderTransform { get; set; } /// /// Gets or sets the render transform origin of the control. diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs index ff1d17164e..44e2e8663b 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Visuals.UnitTests.Media @@ -12,5 +13,46 @@ namespace Avalonia.Visuals.UnitTests.Media var expected = new Matrix(1, 2, 3, -4, 5, 6); Assert.Equal(expected, matrix); } + + [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); + } + + [Fact] + public void Can_Decompose_Angle() + { + var angleRad = MathUtilities.Deg2Rad(30); + + var matrix = Matrix.CreateRotation(angleRad); + + var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); + + Assert.Equal(true, result); + Assert.Equal(angleRad, decomposed.Angle); + } + + [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); + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs new file mode 100644 index 0000000000..8e0520a71d --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs @@ -0,0 +1,128 @@ +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 Can_Interpolate_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); + } + } +} From 179fdd21b33e57f7fa70fc1a8c6b12aa1928a653 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 9 Jun 2020 12:45:42 +0200 Subject: [PATCH 25/55] Use epsilon when checking for singular matrices. Add benchmark for decomposing a Matrix. Add more tests for Matrix struct. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 1 - src/Avalonia.Visuals/Matrix.cs | 11 ++-- .../Visuals/MatrixBenchmarks.cs | 16 ++++++ .../Media/MatrixTests.cs | 53 ++++++++++++++++--- 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index fa5ab2d45b..3115065528 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; namespace Avalonia.Utilities { diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index e18140aa8d..3c8e5e39f2 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -9,6 +9,8 @@ namespace Avalonia /// public readonly struct Matrix : IEquatable { + private const float DecomposeEpsilon = 0.0001f; + private readonly double _m11; private readonly double _m12; private readonly double _m21; @@ -54,7 +56,7 @@ namespace Avalonia /// /// HasInverse Property - returns true if this matrix is invertible, false otherwise. /// - public bool HasInverse => GetDeterminant() != 0; + public bool HasInverse => Math.Abs(GetDeterminant()) >= double.Epsilon; /// /// The first element of the first row @@ -286,7 +288,7 @@ namespace Avalonia { double d = GetDeterminant(); - if (d == 0) + if (Math.Abs(d) < double.Epsilon) { throw new InvalidOperationException("Transform is not invertible."); } @@ -325,8 +327,9 @@ namespace Avalonia decomposed = default; var determinant = matrix.GetDeterminant(); - - if (determinant == 0) + + // Based upon constant in System.Numerics.Matrix4x4. + if (Math.Abs(determinant) < DecomposeEpsilon) { return false; } diff --git a/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs b/tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs new file mode 100644 index 0000000000..17e2237eb0 --- /dev/null +++ b/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); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs index 44e2e8663b..6ef48b6161 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System; using Avalonia.Utilities; using Xunit; @@ -7,13 +7,29 @@ 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() { @@ -26,17 +42,25 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(10, decomposed.Translate.Y); } - [Fact] - public void Can_Decompose_Angle() + [Theory] + [InlineData(30d)] + [InlineData(0d)] + [InlineData(90d)] + [InlineData(270d)] + public void Can_Decompose_Angle(double angleDeg) { - var angleRad = MathUtilities.Deg2Rad(30); + var angleRad = MathUtilities.Deg2Rad(angleDeg); var matrix = Matrix.CreateRotation(angleRad); var result = Matrix.TryDecomposeTransform(matrix, out Matrix.Decomposed decomposed); Assert.Equal(true, result); - Assert.Equal(angleRad, decomposed.Angle); + + var expected = NormalizeAngle(angleRad); + var actual = NormalizeAngle(decomposed.Angle); + + Assert.Equal(expected, actual, 4); } [Theory] @@ -54,5 +78,22 @@ namespace Avalonia.Visuals.UnitTests.Media 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; + } } } From 4b0e88f0eccafe3aaad6e758f8c95236781b4fec Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 9 Jun 2020 12:51:06 +0200 Subject: [PATCH 26/55] Implement remainder of matrix interpolation fallback test. --- .../Media/TransformOperationsTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs index 8e0520a71d..8b4ccba57d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs @@ -115,7 +115,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [Fact] - public void Can_Interpolate_Matrix() + public void Interpolation_Fallback_To_Matrix() { double progress = 0.5d; @@ -123,6 +123,11 @@ namespace Avalonia.Visuals.UnitTests.Media 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); } } } From 0f6bf8d9bae23f7419d48e012d9821a27a017143 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 14 Jun 2020 14:18:51 +0200 Subject: [PATCH 27/55] Cleanup and add comments. --- src/Avalonia.Visuals/Matrix.cs | 11 +++-- .../Transformation/TransformOperation.cs | 43 +++++++++++++++---- .../Transformation/TransformOperations.cs | 8 +++- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 3c8e5e39f2..24ec343106 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -9,8 +9,6 @@ namespace Avalonia /// public readonly struct Matrix : IEquatable { - private const float DecomposeEpsilon = 0.0001f; - private readonly double _m11; private readonly double _m12; private readonly double _m21; @@ -322,14 +320,19 @@ namespace Avalonia } } + /// + /// Decomposes given matrix into transform operations. + /// + /// Matrix to decompose. + /// Decomposed matrix. + /// The status of the operation. public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) { decomposed = default; var determinant = matrix.GetDeterminant(); - // Based upon constant in System.Numerics.Matrix4x4. - if (Math.Abs(determinant) < DecomposeEpsilon) + if (MathUtilities.IsZero(determinant)) { return false; } diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs index cdf31f8e5b..36f5dd98f1 100644 --- a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs @@ -2,6 +2,9 @@ using System.Runtime.InteropServices; namespace Avalonia.Media.Transformation { + /// + /// Represents a single primitive transform (like translation, rotation, scale, etc.). + /// public struct TransformOperation { public OperationType Type; @@ -18,8 +21,14 @@ namespace Avalonia.Media.Transformation Identity } + /// + /// Returns whether operation produces the identity matrix. + /// public bool IsIdentity => Matrix.IsIdentity; + /// + /// Bakes this operation to a transform matrix. + /// public void Bake() { Matrix = Matrix.Identity; @@ -53,11 +62,22 @@ namespace Avalonia.Media.Transformation } } - public static bool IsOperationIdentity(ref TransformOperation? operation) - { - return !operation.HasValue || operation.Value.IsIdentity; - } - + /// + /// Returns new identity transform operation. + /// + public static TransformOperation Identity => + new TransformOperation { Matrix = Matrix.Identity, Type = OperationType.Identity }; + + /// + /// Attempts to interpolate between two transform operations. + /// + /// Source operation. + /// Target operation. + /// Interpolation progress. + /// Interpolation result that will be filled in when operation was successful. + /// + /// Based upon https://www.w3.org/TR/css-transforms-1/#interpolation-of-transform-functions. + /// public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress, ref TransformOperation result) { @@ -69,8 +89,10 @@ namespace Avalonia.Media.Transformation return true; } - TransformOperation fromValue = fromIdentity ? default : from.Value; - TransformOperation toValue = toIdentity ? default : to.Value; + // 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; @@ -139,7 +161,7 @@ namespace Avalonia.Media.Transformation { 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)) { @@ -165,6 +187,11 @@ namespace Avalonia.Media.Transformation return true; } + private static bool IsOperationIdentity(ref TransformOperation? operation) + { + return !operation.HasValue || operation.Value.IsIdentity; + } + [StructLayout(LayoutKind.Explicit)] public struct DataLayout { diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs index 9f711a2d63..334bb93562 100644 --- a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; -using JetBrains.Annotations; namespace Avalonia.Media.Transformation { + /// + /// Contains a list of that represent primitive transforms that will be + /// applied in declared order. + /// public sealed class TransformOperations : ITransform { public static TransformOperations Identity { get; } = new TransformOperations(new List()); @@ -19,6 +22,9 @@ namespace Avalonia.Media.Transformation Value = ApplyTransforms(); } + /// + /// Returns whether all operations combined together produce the identity matrix. + /// public bool IsIdentity { get; } public IReadOnlyList Operations => _operations; From 2b503357277c5e9f62c68f60b8dd68eea0767b04 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 14 Jun 2020 15:08:36 +0200 Subject: [PATCH 28/55] Increase coverage of transform parsing and fix found issues. --- .../Media/Transformation/TransformParser.cs | 31 ++---- .../Media/TransformOperationsTests.cs | 102 +++++++++++++++++- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs index 2a3912832b..85f4f5fec1 100644 --- a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs +++ b/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs @@ -198,16 +198,12 @@ namespace Avalonia.Media.Transformation VerifyZeroOrUnit(function, in scaleX, Unit.None); VerifyZeroOrUnit(function, in scaleY, Unit.None); - if (function == TransformFunction.ScaleX) - { - scaleY = UnitValue.Zero; - } - else if (function == TransformFunction.ScaleY) + if (function == TransformFunction.ScaleY) { scaleY = scaleX; - scaleX = UnitValue.Zero; + scaleX = UnitValue.One; } - else if (count == 1) + else if (function == TransformFunction.Scale && count == 1) { scaleY = scaleX; } @@ -233,19 +229,11 @@ namespace Avalonia.Media.Transformation VerifyZeroOrAngle(function, in skewX); VerifyZeroOrAngle(function, in skewY); - if (function == TransformFunction.SkewX) - { - skewY = UnitValue.Zero; - } - else if (function == TransformFunction.SkewY) + if (function == TransformFunction.SkewY) { skewY = skewX; skewX = UnitValue.Zero; } - else if (count == 1) - { - skewY = skewX; - } builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY)); @@ -286,19 +274,11 @@ namespace Avalonia.Media.Transformation VerifyZeroOrUnit(function, in translateX, Unit.Pixel); VerifyZeroOrUnit(function, in translateY, Unit.Pixel); - if (function == TransformFunction.TranslateX) - { - translateY = UnitValue.Zero; - } - else if (function == TransformFunction.TranslateY) + if (function == TransformFunction.TranslateY) { translateY = translateX; translateX = UnitValue.Zero; } - else if (count == 1) - { - translateY = translateX; - } builder.AppendTranslate(translateX.Value, translateY.Value); @@ -358,6 +338,7 @@ namespace Avalonia.Media.Transformation switch (unit) { case Unit.Radian: + case Unit.Gradian: case Unit.Degree: case Unit.Turn: { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs index 8b4ccba57d..856b4615a5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TransformOperationsTests.cs @@ -6,6 +6,93 @@ 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() { @@ -37,6 +124,15 @@ namespace Avalonia.Visuals.UnitTests.Media 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] @@ -59,9 +155,9 @@ namespace Avalonia.Visuals.UnitTests.Media } [Theory] - [InlineData(0d, 10d, 0d)] - [InlineData(0.5d, 5d, 10d)] - [InlineData(1d, 0d, 20d)] + [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)"); From 86062c40ae2c991bb7b327e246d494432232d466 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 14 Jun 2020 16:03:01 +0200 Subject: [PATCH 29/55] Cleanup transform animator. --- .../Animators/TransformOperationsAnimator.cs | 26 +++++++++---------- .../TransformOperationsTransition.cs | 5 +++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs index f45338122f..8e9d20eb8f 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs @@ -4,32 +4,32 @@ using Avalonia.Media.Transformation; namespace Avalonia.Animation.Animators { - public class TransformOperationsAnimator : Animator + public class TransformOperationsAnimator : Animator { 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) + public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue) { - var oldTransform = Cast(oldValue); - var newTransform = Cast(newValue); + var oldTransform = EnsureOperations(oldValue); + var newTransform = EnsureOperations(newValue); return TransformOperations.Interpolate(oldTransform, newTransform, progress); } - private static TransformOperations Cast(ITransform value) + 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)}."); + } + } } } diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs index 4911b34d91..104acb71ad 100644 --- a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs +++ b/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs @@ -13,12 +13,15 @@ namespace Avalonia.Animation 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, oldValue, newValue); + return _operationsAnimator.Interpolate(f, oldTransform, newTransform); }); } } From 3b8cd5dae282c4168e6de8bbc9b5f83f1f51e223 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 14 Jun 2020 16:03:10 +0200 Subject: [PATCH 30/55] Add transitions example. --- samples/RenderDemo/MainWindow.xaml | 3 + samples/RenderDemo/Pages/TransitionsPage.xaml | 101 ++++++++++++++++++ .../RenderDemo/Pages/TransitionsPage.xaml.cs | 37 +++++++ 3 files changed, 141 insertions(+) create mode 100644 samples/RenderDemo/Pages/TransitionsPage.xaml create mode 100644 samples/RenderDemo/Pages/TransitionsPage.xaml.cs diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index c098ef411e..14ccc82043 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -29,6 +29,9 @@ + + + diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml new file mode 100644 index 0000000000..df7130a925 --- /dev/null +++ b/samples/RenderDemo/Pages/TransitionsPage.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hover to activate Transform Keyframe Animations. +