From 95735d239b83c71e1c854827f1911e4aeccccd9f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 21 Apr 2020 16:17:52 +0200 Subject: [PATCH 001/108] 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 89fa3b77f5ae91e6c9dba253fef5ac7482850a3d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 May 2020 11:36:03 +0200 Subject: [PATCH 002/108] Added nullable annotations to ContextMenu. --- src/Avalonia.Controls/ContextMenu.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 86499530da..a026c8fa72 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; @@ -9,9 +8,10 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Styling; +#nullable enable + namespace Avalonia.Controls { /// @@ -21,9 +21,9 @@ namespace Avalonia.Controls { private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); - private Popup _popup; - private List _attachedControls; - private IInputElement _previousFocus; + private Popup? _popup; + private List? _attachedControls; + private IInputElement? _previousFocus; /// /// Initializes a new instance of the class. @@ -56,14 +56,14 @@ namespace Avalonia.Controls /// /// property is changing from false to true. /// - public event CancelEventHandler ContextMenuOpening; + public event CancelEventHandler? ContextMenuOpening; /// /// Occurs when the value of the /// /// property is changing from true to false. /// - public event CancelEventHandler ContextMenuClosing; + public event CancelEventHandler? ContextMenuClosing; /// /// Called when the property changes on a control. @@ -77,7 +77,7 @@ namespace Avalonia.Controls { control.PointerReleased -= ControlPointerReleased; oldMenu._attachedControls?.Remove(control); - ((ISetLogicalParent)oldMenu._popup)?.SetParent(null); + ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null); } if (e.NewValue is ContextMenu newMenu) @@ -97,7 +97,7 @@ namespace Avalonia.Controls /// Opens a context menu on the specified control. /// /// The control. - public void Open(Control control) + public void Open(Control? control) { if (control is null && (_attachedControls is null || _attachedControls.Count == 0)) { @@ -113,7 +113,7 @@ namespace Avalonia.Controls nameof(control)); } - control ??= _attachedControls[0]; + control ??= _attachedControls![0]; if (IsOpen) { @@ -204,7 +204,7 @@ namespace Avalonia.Controls if (_attachedControls is null || _attachedControls.Count == 0) { - ((ISetLogicalParent)_popup).SetParent(null); + ((ISetLogicalParent)_popup!).SetParent(null); } // HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic. From 372042b6f9afb67ec2934f5a76dd3f6442bad1fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 May 2020 11:39:12 +0200 Subject: [PATCH 003/108] Added positioning properties to ContextMenu. --- src/Avalonia.Controls/ContextMenu.cs | 69 ++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index a026c8fa72..5a60fd37af 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -19,6 +19,30 @@ namespace Avalonia.Controls /// public class ContextMenu : MenuBase, ISetterValue { + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementModeProperty = + Popup.PlacementModeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalOffsetProperty = + Popup.HorizontalOffsetProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalOffsetProperty = + Popup.VerticalOffsetProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementTargetProperty = + Popup.PlacementTargetProperty.AddOwner(); + private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); private Popup? _popup; @@ -47,10 +71,47 @@ namespace Avalonia.Controls /// static ContextMenu() { - ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); } + /// + /// Gets or sets the placement mode of the popup in relation to the . + /// + public PlacementMode PlacementMode + { + get { return GetValue(PlacementModeProperty); } + set { SetValue(PlacementModeProperty, value); } + } + + /// + /// Gets or sets the Horizontal offset of the popup in relation to the + /// + public double HorizontalOffset + { + get { return GetValue(HorizontalOffsetProperty); } + set { SetValue(HorizontalOffsetProperty, value); } + } + + /// + /// Gets or sets the Vertical offset of the popup in relation to the + /// + public double VerticalOffset + { + get { return GetValue(VerticalOffsetProperty); } + set { SetValue(VerticalOffsetProperty, value); } + } + + /// + /// Gets or sets the control that is used to determine the popup's position. + /// + public Control? PlacementTarget + { + get { return GetValue(PlacementTargetProperty); } + set { SetValue(PlacementTargetProperty, value); } + } + /// /// Occurs when the value of the /// @@ -124,8 +185,10 @@ namespace Avalonia.Controls { _popup = new Popup { - PlacementMode = PlacementMode.Pointer, - PlacementTarget = control, + HorizontalOffset = HorizontalOffset, + VerticalOffset = VerticalOffset, + PlacementMode = PlacementMode, + PlacementTarget = PlacementTarget ?? control, StaysOpen = false }; From 9bb71a248467f2827518b652797441e647d7ed30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 May 2020 14:46:49 +0200 Subject: [PATCH 004/108] Added/tweaked docs. --- .../Primitives/IPopupHost.cs | 46 +++ .../PopupPositioning/IPopupPositioner.cs | 269 +++++++++++------- .../ManagedPopupPositioner.cs | 4 + 3 files changed, 218 insertions(+), 101 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index 74a3ca8818..bb4daf38e1 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -5,19 +5,65 @@ using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { + /// + /// Represents the top-level control opened by a . + /// + /// + /// A popup host can be either be a popup window created by the operating system + /// () or an which is created + /// on an . + /// public interface IPopupHost : IDisposable { + /// + /// Sets the control to display in the popup. + /// + /// void SetChild(IControl control); + + /// + /// Gets the presenter from the control's template. + /// IContentPresenter Presenter { get; } + + /// + /// Gets the root of the visual tree in the case where the popup is presented using a + /// separate visual tree. + /// IVisual HostedVisualTreeRoot { get; } + /// + /// Raised when the control's template is applied. + /// event EventHandler TemplateApplied; + /// + /// Configures the position of the popup according to a target control and a set of + /// placement parameters. + /// + /// The placement target. + /// The placement mode. + /// The offset, in device-independent pixels. + /// The anchor point. + /// The anchor gravity. void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None); + + /// + /// Shows the popup. + /// void Show(); + + /// + /// Hides the popup. + /// void Hide(); + + /// + /// Binds the constraints of the popup host to a set of properties, usally those present on + /// . + /// IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index f0358ec04f..d2a403b602 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -50,45 +50,47 @@ using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives.PopupPositioning { /// - /// - /// The IPopupPositioner provides a collection of rules for the placement of a - /// a popup relative to its parent. Rules can be defined to ensure - /// the popup remains within the visible area's borders, and to - /// specify how the popup changes its position, such as sliding along - /// an axis, or flipping around a rectangle. These positioner-created rules are - /// constrained by the requirement that a popup must intersect with or - /// be at least partially adjacent to its parent surface. + /// Provides positioning parameters to . /// + /// + /// The IPopupPositioner provides a collection of rules for the placement of a a popup relative + /// to its parent. Rules can be defined to ensure the popup remains within the visible area's + /// borders, and to specify how the popup changes its position, such as sliding along an axis, + /// or flipping around a rectangle. These positioner-created rules are constrained by the + /// requirement that a popup must intersect with or be at least partially adjacent to its parent + /// surface. + /// public struct PopupPositionerParameters { private PopupPositioningEdge _gravity; private PopupPositioningEdge _anchor; /// - /// Set the size of the popup that is to be positioned with the positioner - /// object. The size is in scaled coordinates. + /// Set the size of the popup that is to be positioned with the positioner object, in device- + /// independent pixels. /// public Size Size { get; set; } /// - /// Specify the anchor rectangle within the parent that the popup - /// will be placed relative to. The rectangle is relative to the - /// parent geometry - /// - /// The anchor rectangle may not extend outside the window geometry of the - /// popup's parent. The anchor rectangle is in scaled coordinates + /// Specifies the anchor rectangle within the parent that the popup will be placed relative + /// to, in device-independent pixels. /// + /// + /// The rectangle is relative to the parent geometry and may not extend outside the window + /// geometry of the popup's parent. + /// public Rect AnchorRectangle { get; set; } - /// - /// Defines the anchor point for the anchor rectangle. The specified anchor - /// is used derive an anchor point that the popup will be - /// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or - /// 'BottomRight'), the anchor point will be at the specified corner; - /// otherwise, the derived anchor point will be centered on the specified - /// edge, or in the center of the anchor rectangle if no edge is specified. + /// Defines the anchor point for the anchor rectangle. /// + /// + /// The specified anchor is used derive an anchor point that the popup will be positioned + /// relative to. If a corner anchor is set (e.g. 'TopLeft' or 'BottomRight'), the anchor + /// point will be at the specified corner; otherwise, the derived anchor point will be + /// centered on the specified edge, or in the center of the anchor rectangle if no edge is + /// specified. + /// public PopupPositioningEdge Anchor { get => _anchor; @@ -100,13 +102,14 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } /// - /// Defines in what direction a popup should be positioned, relative to - /// the anchor point of the parent. If a corner gravity is - /// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup - /// will be placed towards the specified gravity; otherwise, the popup - /// will be centered over the anchor point on any axis that had no - /// gravity specified. + /// Defines in what direction a popup should be positioned, relative to the anchor point of + /// the parent. /// + /// + /// If a corner gravity is specified (e.g. 'BottomRight' or 'TopLeft'), then the popup will + /// be placed towards the specified gravity; otherwise, the popup will be centered over the + /// anchor point on any axis that had no gravity specified. + /// public PopupPositioningEdge Gravity { get => _gravity; @@ -118,48 +121,51 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } /// - /// Specify how the popup should be positioned if the originally intended - /// position caused the popup to be constrained, meaning at least - /// partially outside positioning boundaries set by the positioner. The - /// adjustment is set by constructing a bitmask describing the adjustment to - /// be made when the popup is constrained on that axis. + /// Specify how the popup should be positioned if the originally intended position caused + /// the popup to be constrained. + /// + /// + /// Adjusts the popup position if the intended position caused the popup to be constrained; + /// meaning at least partially outside positioning boundaries set by the positioner. The + /// adjustment is set by constructing a bitmask describing the adjustment to be made when + /// the popup is constrained on that axis. /// - /// If no bit for one axis is set, the positioner will assume that the child - /// surface should not change its position on that axis when constrained. + /// If no bit for one axis is set, the positioner will assume that the child surface should + /// not change its position on that axis when constrained. /// - /// If more than one bit for one axis is set, the order of how adjustments - /// are applied is specified in the corresponding adjustment descriptions. + /// If more than one bit for one axis is set, the order of how adjustments are applied is + /// specified in the corresponding adjustment descriptions. /// /// The default adjustment is none. - /// + /// public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; } - + /// /// Specify the popup position offset relative to the position of the - /// anchor on the anchor rectangle and the anchor on the popup. For - /// example if the anchor of the anchor rectangle is at (x, y), the popup - /// has the gravity bottom|right, and the offset is (ox, oy), the calculated - /// surface position will be (x + ox, y + oy). The offset position of the - /// surface is the one used for constraint testing. See - /// set_constraint_adjustment. - /// - /// An example use case is placing a popup menu on top of a user interface - /// element, while aligning the user interface element of the parent surface - /// with some user interface element placed somewhere in the popup. + /// anchor on the anchor rectangle and the anchor on the popup. /// + /// + /// For example if the anchor of the anchor rectangle is at (x, y), the popup has the + /// gravity bottom|right, and the offset is (ox, oy), the calculated surface position will + /// be (x + ox, y + oy). The offset position of the surface is the one used for constraint + /// testing. See set_constraint_adjustment. + /// + /// An example use case is placing a popup menu on top of a user interface element, while + /// aligning the user interface element of the parent surface with some user interface + /// element placed somewhere in the popup. + /// public Point Offset { get; set; } } - + /// - /// The constraint adjustment value define ways how popup position will - /// be adjusted if the unadjusted position would result in the popup - /// being partly constrained. - /// - /// Whether a popup is considered 'constrained' is left to the positioner - /// to determine. For example, the popup may be partly outside the - /// target platform defined 'work area', thus necessitating the popup's - /// position be adjusted until it is entirely inside the work area. + /// Defines how a popup position will be adjusted if the unadjusted position would result in + /// the popup being partly constrained. /// + /// + /// Whether a popup is considered 'constrained' is left to the positioner to determine. For + /// example, the popup may be partly outside the target platform defined 'work area', thus + /// necessitating the popup's position be adjusted until it is entirely inside the work area. + /// [Flags] public enum PopupPositionerConstraintAdjustment { @@ -171,62 +177,59 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// /// Slide the surface along the x axis until it is no longer constrained. - /// First try to slide towards the direction of the gravity on the x axis - /// until either the edge in the opposite direction of the gravity is - /// unconstrained or the edge in the direction of the gravity is - /// constrained. - /// - /// Then try to slide towards the opposite direction of the gravity on the - /// x axis until either the edge in the direction of the gravity is - /// unconstrained or the edge in the opposite direction of the gravity is - /// constrained. /// + /// + /// First try to slide towards the direction of the gravity on the x axis until either the + /// edge in the opposite direction of the gravity is unconstrained or the edge in the + /// direction of the gravity is constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the x axis until + /// either the edge in the direction of the gravity is unconstrained or the edge in the + /// opposite direction of the gravity is constrained. + /// SlideX = 1, - /// - /// Slide the surface along the y axis until it is no longer constrained. - /// - /// First try to slide towards the direction of the gravity on the y axis - /// until either the edge in the opposite direction of the gravity is - /// unconstrained or the edge in the direction of the gravity is - /// constrained. - /// - /// Then try to slide towards the opposite direction of the gravity on the - /// y axis until either the edge in the direction of the gravity is - /// unconstrained or the edge in the opposite direction of the gravity is - /// constrained. - /// */ + /// Slide the surface along the y axis until it is no longer constrained. /// + /// + /// First try to slide towards the direction of the gravity on the y axis until either the + /// edge in the opposite direction of the gravity is unconstrained or the edge in the + /// direction of the gravity is constrained. + /// + /// Then try to slide towards the opposite direction of the gravity on the y axis until + /// either the edge in the direction of the gravity is unconstrained or the edge in the + /// opposite direction of the gravity is constrained. + /// SlideY = 2, /// - /// Invert the anchor and gravity on the x axis if the surface is - /// constrained on the x axis. For example, if the left edge of the - /// surface is constrained, the gravity is 'left' and the anchor is - /// 'left', change the gravity to 'right' and the anchor to 'right'. - /// - /// If the adjusted position also ends up being constrained, the resulting - /// position of the flip_x adjustment will be the one before the - /// adjustment. + /// Invert the anchor and gravity on the x axis if the surface is constrained on the x axis. /// + /// + /// For example, if the left edge of the surface is constrained, the gravity is 'left' and + /// the anchor is 'left', change the gravity to 'right' and the anchor to 'right'. + /// + /// If the adjusted position also ends up being constrained, the resulting position of the + /// FlipX adjustment will be the one before the adjustment. + /// /// FlipX = 4, /// - /// Invert the anchor and gravity on the y axis if the surface is - /// constrained on the y axis. For example, if the bottom edge of the - /// surface is constrained, the gravity is 'bottom' and the anchor is - /// 'bottom', change the gravity to 'top' and the anchor to 'top'. + /// Invert the anchor and gravity on the y axis if the surface is constrained on the y axis. + /// + /// + /// For example, if the bottom edge of the surface is constrained, the gravity is 'bottom' + /// and the anchor is 'bottom', change the gravity to 'top' and the anchor to 'top'. /// - /// The adjusted position is calculated given the original anchor - /// rectangle and offset, but with the new flipped anchor and gravity - /// values. + /// The adjusted position is calculated given the original anchor rectangle and offset, but + /// with the new flipped anchor and gravity values. /// - /// If the adjusted position also ends up being constrained, the resulting - /// position of the flip_y adjustment will be the one before the - /// adjustment. - /// + /// If the adjusted position also ends up being constrained, the resulting position of the + /// FlipY adjustment will be the one before the adjustment. + /// FlipY = 8, + All = SlideX|SlideY|FlipX|FlipY } @@ -267,27 +270,91 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } + /// + /// Defines the popup position edge for and + /// . + /// [Flags] public enum PopupPositioningEdge { + /// + /// The center of the anchor rectangle. + /// None, + + /// + /// The top edge of the anchor rectangle. + /// Top = 1, + + /// + /// The bottom edge of the anchor rectangle. + /// Bottom = 2, + + /// + /// The left edge of the anchor rectangle. + /// Left = 4, + + /// + /// The right edge of the anchor rectangle. + /// Right = 8, + + /// + /// The top-left corner of the anchor rectangle. + /// TopLeft = Top | Left, + + /// + /// The top-right corner of the anchor rectangle. + /// TopRight = Top | Right, + + /// + /// The bottom-left corner of the anchor rectangle. + /// BottomLeft = Bottom | Left, + + /// + /// The bottom-right corner of the anchor rectangle. + /// BottomRight = Bottom | Right, - + /// + /// A mask for the vertical component flags. + /// VerticalMask = Top | Bottom, + + /// + /// A mask for the horizontal component flags. + /// HorizontalMask = Left | Right, + + /// + /// A mask for all flags. + /// AllMask = VerticalMask|HorizontalMask } + /// + /// Positions an . + /// + /// + /// is an abstraction of the wayland xdg_positioner spec. + /// + /// The popup positioner implementation is determined by the platform implementation. A default + /// managed implementation is provided in for platforms + /// on which popups can be arbitrarily positioned. + /// public interface IPopupPositioner { + /// + /// Updates the position of the associated according to the + /// specified parameters. + /// + /// The positioning parameters. void Update(PopupPositionerParameters parameters); } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index 07348cdf78..bd23a13e7f 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -25,6 +25,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } } + /// + /// An implementation for platforms on which a popup can be + /// aritrarily positioned. + /// public class ManagedPopupPositioner : IPopupPositioner { private readonly IManagedPopupPositionerPopup _popup; From 07d1f16fa0f7eeb58c35c9977bab06f4c9a6fa15 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 May 2020 15:35:25 +0200 Subject: [PATCH 005/108] Added additional placement properties to PopupRoot. And split `PopupPositioningEdge` into `PopupAnchor` and `PopupGravity` so their values can be documented more clearly. --- .../Primitives/IPopupHost.cs | 10 +- .../Primitives/OverlayPopupHost.cs | 5 +- src/Avalonia.Controls/Primitives/Popup.cs | 97 +++++++++--- .../PopupPositioning/IPopupPositioner.cs | 138 +++++++++++++----- .../ManagedPopupPositioner.cs | 42 +++--- src/Avalonia.Controls/Primitives/PopupRoot.cs | 7 +- 6 files changed, 215 insertions(+), 84 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index bb4daf38e1..18fb45fd5a 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -45,10 +45,14 @@ namespace Avalonia.Controls.Primitives /// The placement mode. /// The offset, in device-independent pixels. /// The anchor point. - /// The anchor gravity. + /// The popup gravity. + /// + /// The anchor rect. If null, the bounds of will be used. + /// void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, - PopupPositioningEdge anchor = PopupPositioningEdge.None, - PopupPositioningEdge gravity = PopupPositioningEdge.None); + PopupAnchor anchor = PopupAnchor.None, + PopupGravity gravity = PopupGravity.None, + Rect? rect = null); /// /// Shows the popup. diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 3dc9d302db..15b09f29f1 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -71,10 +71,11 @@ namespace Avalonia.Controls.Primitives } public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, - PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None) + PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None, + Rect? rect = null) { _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor, - gravity); + gravity, rect); UpdatePosition(); } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 66f2153b6c..ff1201286c 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -34,12 +35,36 @@ namespace Avalonia.Controls.Primitives o => o.IsOpen, (o, v) => o.IsOpen = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementAnchorProperty = + AvaloniaProperty.Register(nameof(PlacementAnchor)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementGravityProperty = + AvaloniaProperty.Register(nameof(PlacementGravity)); + /// /// Defines the property. /// public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementRectProperty = + AvaloniaProperty.Register(nameof(PlacementRect)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementTargetProperty = + AvaloniaProperty.Register(nameof(PlacementTarget)); + #pragma warning disable 618 /// /// Defines the property. @@ -60,12 +85,6 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty VerticalOffsetProperty = AvaloniaProperty.Register(nameof(VerticalOffset)); - /// - /// Defines the property. - /// - public static readonly StyledProperty PlacementTargetProperty = - AvaloniaProperty.Register(nameof(PlacementTarget)); - /// /// Defines the property. /// @@ -136,6 +155,26 @@ namespace Avalonia.Controls.Primitives set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } } + /// + /// Gets or sets the anchor point on the when + /// is . + /// + public PopupAnchor PlacementAnchor + { + get { return GetValue(PlacementAnchorProperty); } + set { SetValue(PlacementAnchorProperty, value); } + } + + /// + /// Gets or sets a value which defines in what direction the popup should open + /// when is . + /// + public PopupGravity PlacementGravity + { + get { return GetValue(PlacementGravityProperty); } + set { SetValue(PlacementGravityProperty, value); } + } + /// /// Gets or sets the placement mode of the popup in relation to the . /// @@ -145,6 +184,32 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } + /// + /// Gets or sets the the anchor rectangle within the parent that the popup will be placed + /// relative to when is . + /// + /// + /// The placement rect defines a rectangle relative to around + /// which the popup will be opened, with determining which edge + /// of the placement target is used. + /// + /// If unset, the anchor rectangle will be the bounds of the . + /// + public Rect? PlacementRect + { + get { return GetValue(PlacementRectProperty); } + set { SetValue(PlacementRectProperty, value); } + } + + /// + /// Gets or sets the control that is used to determine the popup's position. + /// + public Control? PlacementTarget + { + get { return GetValue(PlacementTargetProperty); } + set { SetValue(PlacementTargetProperty, value); } + } + [Obsolete("This property has no effect")] public bool ObeyScreenEdges { @@ -153,7 +218,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets or sets the Horizontal offset of the popup in relation to the + /// Gets or sets the Horizontal offset of the popup in relation to the . /// public double HorizontalOffset { @@ -162,7 +227,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets or sets the Vertical offset of the popup in relation to the + /// Gets or sets the Vertical offset of the popup in relation to the . /// public double VerticalOffset { @@ -170,15 +235,6 @@ namespace Avalonia.Controls.Primitives set { SetValue(VerticalOffsetProperty, value); } } - /// - /// Gets or sets the control that is used to determine the popup's position. - /// - public Control? PlacementTarget - { - get { return GetValue(PlacementTargetProperty); } - set { SetValue(PlacementTargetProperty, value); } - } - /// /// Gets or sets a value indicating whether the popup should stay open when the popup is /// pressed or loses focus. @@ -251,8 +307,11 @@ namespace Avalonia.Controls.Primitives popupHost.ConfigurePosition( placementTarget, - PlacementMode, - new Point(HorizontalOffset, VerticalOffset)); + PlacementMode, + new Point(HorizontalOffset, VerticalOffset), + PlacementAnchor, + PlacementGravity, + PlacementRect); DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index d2a403b602..d6e54b80c1 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -62,8 +62,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// public struct PopupPositionerParameters { - private PopupPositioningEdge _gravity; - private PopupPositioningEdge _anchor; + private PopupGravity _gravity; + private PopupAnchor _anchor; /// /// Set the size of the popup that is to be positioned with the positioner object, in device- @@ -91,7 +91,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// centered on the specified edge, or in the center of the anchor rectangle if no edge is /// specified. /// - public PopupPositioningEdge Anchor + public PopupAnchor Anchor { get => _anchor; set @@ -110,12 +110,12 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// be placed towards the specified gravity; otherwise, the popup will be centered over the /// anchor point on any axis that had no gravity specified. /// - public PopupPositioningEdge Gravity + public PopupGravity Gravity { get => _gravity; set { - PopupPositioningEdgeHelper.ValidateEdge(value); + PopupPositioningEdgeHelper.ValidateGravity(value); _gravity = value; } } @@ -235,18 +235,23 @@ namespace Avalonia.Controls.Primitives.PopupPositioning static class PopupPositioningEdgeHelper { - public static void ValidateEdge(this PopupPositioningEdge edge) + public static void ValidateEdge(this PopupAnchor edge) { - if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0) + if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0) || - ((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0)) + ((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0)) throw new ArgumentException("Opposite edges specified"); } - public static PopupPositioningEdge Flip(this PopupPositioningEdge edge) + public static void ValidateGravity(this PopupGravity gravity) + { + ValidateEdge((PopupAnchor)gravity); + } + + public static PopupAnchor Flip(this PopupAnchor edge) { - var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right; - var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom; + var hmask = PopupAnchor.Left | PopupAnchor.Right; + var vmask = PopupAnchor.Top | PopupAnchor.Bottom; if ((edge & hmask) != 0) edge ^= hmask; if ((edge & vmask) != 0) @@ -254,28 +259,36 @@ namespace Avalonia.Controls.Primitives.PopupPositioning return edge; } - public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge) + public static PopupAnchor FlipX(this PopupAnchor edge) { - if ((edge & PopupPositioningEdge.HorizontalMask) != 0) - edge ^= PopupPositioningEdge.HorizontalMask; + if ((edge & PopupAnchor.HorizontalMask) != 0) + edge ^= PopupAnchor.HorizontalMask; return edge; } - public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge) + public static PopupAnchor FlipY(this PopupAnchor edge) { - if ((edge & PopupPositioningEdge.VerticalMask) != 0) - edge ^= PopupPositioningEdge.VerticalMask; + if ((edge & PopupAnchor.VerticalMask) != 0) + edge ^= PopupAnchor.VerticalMask; return edge; } - + + public static PopupGravity FlipX(this PopupGravity gravity) + { + return (PopupGravity)FlipX((PopupAnchor)gravity); + } + + public static PopupGravity FlipY(this PopupGravity gravity) + { + return (PopupGravity)FlipY((PopupAnchor)gravity); + } } /// - /// Defines the popup position edge for and - /// . + /// Defines the edges around an anchor rectangle on which a popup will open. /// [Flags] - public enum PopupPositioningEdge + public enum PopupAnchor { /// /// The center of the anchor rectangle. @@ -338,6 +351,58 @@ namespace Avalonia.Controls.Primitives.PopupPositioning AllMask = VerticalMask|HorizontalMask } + /// + /// Defines the direction in which a popup will open. + /// + [Flags] + public enum PopupGravity + { + /// + /// The popup will be centered over the anchor edge. + /// + None, + + /// + /// The popup will be positioned above the anchor edge + /// + Top = 1, + + /// + /// The popup will be positioned below the anchor edge + /// + Bottom = 2, + + /// + /// The popup will be positioned to the left of the anchor edge + /// + Left = 4, + + /// + /// The popup will be positioned to the right of the anchor edge + /// + Right = 8, + + /// + /// The popup will be positioned to the top-left of the anchor edge + /// + TopLeft = Top | Left, + + /// + /// The popup will be positioned to the top-right of the anchor edge + /// + TopRight = Top | Right, + + /// + /// The popup will be positioned to the bottom-left of the anchor edge + /// + BottomLeft = Bottom | Left, + + /// + /// The popup will be positioned to the bottom-right of the anchor edge + /// + BottomRight = Bottom | Right, + } + /// /// Positions an . /// @@ -363,7 +428,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters, TopLevel topLevel, IVisual target, PlacementMode placement, Point offset, - PopupPositioningEdge anchor, PopupPositioningEdge gravity) + PopupAnchor anchor, PopupGravity gravity, Rect? rect) { // We need a better way for tracking the last pointer position var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position); @@ -373,8 +438,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (placement == PlacementMode.Pointer) { positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); - positionerParameters.Anchor = PopupPositioningEdge.TopLeft; - positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + positionerParameters.Anchor = PopupAnchor.TopLeft; + positionerParameters.Gravity = PopupGravity.BottomRight; } else { @@ -384,32 +449,33 @@ namespace Avalonia.Controls.Primitives.PopupPositioning if (matrix == null) { if (target.GetVisualRoot() == null) - throw new InvalidCastException("Target control is not attached to the visual tree"); - throw new InvalidCastException("Target control is not in the same tree as the popup parent"); + throw new InvalidOperationException("Target control is not attached to the visual tree"); + throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); } - positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) - .TransformToAABB(matrix.Value); + var bounds = new Rect(default, target.Bounds.Size); + var anchorRect = rect ?? bounds; + positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); if (placement == PlacementMode.Right) { - positionerParameters.Anchor = PopupPositioningEdge.TopRight; - positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + positionerParameters.Anchor = PopupAnchor.TopRight; + positionerParameters.Gravity = PopupGravity.BottomRight; } else if (placement == PlacementMode.Bottom) { - positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; - positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + positionerParameters.Anchor = PopupAnchor.BottomLeft; + positionerParameters.Gravity = PopupGravity.BottomRight; } else if (placement == PlacementMode.Left) { - positionerParameters.Anchor = PopupPositioningEdge.TopLeft; - positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; + positionerParameters.Anchor = PopupAnchor.TopLeft; + positionerParameters.Gravity = PopupGravity.BottomLeft; } else if (placement == PlacementMode.Top) { - positionerParameters.Anchor = PopupPositioningEdge.TopLeft; - positionerParameters.Gravity = PopupPositioningEdge.TopRight; + positionerParameters.Anchor = PopupAnchor.TopLeft; + positionerParameters.Gravity = PopupGravity.TopRight; } else if (placement == PlacementMode.AnchorAndGravity) { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index bd23a13e7f..04167bbd15 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -39,38 +39,38 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } - private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge) + private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge) { double x, y; - if ((edge & PopupPositioningEdge.Left) != 0) + if ((edge & PopupAnchor.Left) != 0) x = anchorRect.X; - else if ((edge & PopupPositioningEdge.Right) != 0) + else if ((edge & PopupAnchor.Right) != 0) x = anchorRect.Right; else x = anchorRect.X + anchorRect.Width / 2; - if ((edge & PopupPositioningEdge.Top) != 0) + if ((edge & PopupAnchor.Top) != 0) y = anchorRect.Y; - else if ((edge & PopupPositioningEdge.Bottom) != 0) + else if ((edge & PopupAnchor.Bottom) != 0) y = anchorRect.Bottom; else y = anchorRect.Y + anchorRect.Height / 2; return new Point(x, y); } - private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity) + private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity) { double x, y; - if ((gravity & PopupPositioningEdge.Left) != 0) + if ((gravity & PopupGravity.Left) != 0) x = -size.Width; - else if ((gravity & PopupPositioningEdge.Right) != 0) + else if ((gravity & PopupGravity.Right) != 0) x = 0; else x = -size.Width / 2; - if ((gravity & PopupPositioningEdge.Top) != 0) + if ((gravity & PopupGravity.Top) != 0) y = -size.Height; - else if ((gravity & PopupPositioningEdge.Bottom) != 0) + else if ((gravity & PopupGravity.Bottom) != 0) y = 0; else y = -size.Height / 2; @@ -89,7 +89,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning private void Update(Size translatedSize, Size originalSize, - Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity, + Rect anchorRect, PopupAnchor anchor, PopupGravity gravity, PopupPositionerConstraintAdjustment constraintAdjustment, Point offset) { var parentGeometry = _popup.ParentClientAreaScreenGeometry; @@ -116,28 +116,28 @@ namespace Avalonia.Controls.Primitives.PopupPositioning var bounds = GetBounds(); - bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask) + bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask) { - if ((edge & PopupPositioningEdge.Left) != 0 + if ((edge & PopupAnchor.Left) != 0 && rc.X < bounds.X) return false; - if ((edge & PopupPositioningEdge.Top) != 0 + if ((edge & PopupAnchor.Top) != 0 && rc.Y < bounds.Y) return false; - if ((edge & PopupPositioningEdge.Right) != 0 + if ((edge & PopupAnchor.Right) != 0 && rc.Right > bounds.Right) return false; - if ((edge & PopupPositioningEdge.Bottom) != 0 + if ((edge & PopupAnchor.Bottom) != 0 && rc.Bottom > bounds.Bottom) return false; return true; } - Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) => + Rect GetUnconstrained(PopupAnchor a, PopupGravity g) => new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize); @@ -145,11 +145,11 @@ namespace Avalonia.Controls.Primitives.PopupPositioning // If flipping geometry and anchor is allowed and helps, use the flipped one, // otherwise leave it as is - if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask) + if (!FitsInBounds(geo, PopupAnchor.HorizontalMask) && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) { var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); - if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask)) + if (FitsInBounds(flipped, PopupAnchor.HorizontalMask)) geo = geo.WithX(flipped.X); } @@ -163,11 +163,11 @@ namespace Avalonia.Controls.Primitives.PopupPositioning // If flipping geometry and anchor is allowed and helps, use the flipped one, // otherwise leave it as is - if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask) + if (!FitsInBounds(geo, PopupAnchor.VerticalMask) && (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) { var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); - if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask)) + if (FitsInBounds(flipped, PopupAnchor.VerticalMask)) geo = geo.WithY(flipped.Y); } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 788fe03162..3b167d52e1 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -82,11 +82,12 @@ namespace Avalonia.Controls.Primitives } public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, - PopupPositioningEdge anchor = PopupPositioningEdge.None, - PopupPositioningEdge gravity = PopupPositioningEdge.None) + PopupAnchor anchor = PopupAnchor.None, + PopupGravity gravity = PopupGravity.None, + Rect? rect = null) { _positionerParameters.ConfigurePosition(_parent, target, - placement, offset, anchor, gravity); + placement, offset, anchor, gravity, rect); if (_positionerParameters.Size != default) UpdatePosition(); From 4334c124b9ad177228d64e3ce7f1c7de6f12b889 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 May 2020 15:43:17 +0200 Subject: [PATCH 006/108] Added additional placement properties to ContextMenu. --- src/Avalonia.Controls/ContextMenu.cs | 92 +++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 5a60fd37af..286cb6d6d8 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; @@ -19,11 +20,6 @@ namespace Avalonia.Controls /// public class ContextMenu : MenuBase, ISetterValue { - /// - /// Defines the property. - /// - public static readonly StyledProperty PlacementModeProperty = - Popup.PlacementModeProperty.AddOwner(); /// /// Defines the property. @@ -37,6 +33,30 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalOffsetProperty = Popup.VerticalOffsetProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementAnchorProperty = + Popup.PlacementAnchorProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementGravityProperty = + Popup.PlacementGravityProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementModeProperty = + Popup.PlacementModeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlacementRectProperty = + AvaloniaProperty.Register(nameof(PlacementRect)); + /// /// Defines the property. /// @@ -77,16 +97,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the placement mode of the popup in relation to the . - /// - public PlacementMode PlacementMode - { - get { return GetValue(PlacementModeProperty); } - set { SetValue(PlacementModeProperty, value); } - } - - /// - /// Gets or sets the Horizontal offset of the popup in relation to the + /// Gets or sets the Horizontal offset of the context menu in relation to the . /// public double HorizontalOffset { @@ -95,7 +106,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the Vertical offset of the popup in relation to the + /// Gets or sets the Vertical offset of the popup in relation to the . /// public double VerticalOffset { @@ -103,6 +114,52 @@ namespace Avalonia.Controls set { SetValue(VerticalOffsetProperty, value); } } + /// + /// Gets or sets the anchor point on the when + /// is . + /// + public PopupAnchor PlacementAnchor + { + get { return GetValue(PlacementAnchorProperty); } + set { SetValue(PlacementAnchorProperty, value); } + } + + /// + /// Gets or sets a value which defines in what direction the context menu should open + /// when is . + /// + public PopupGravity PlacementGravity + { + get { return GetValue(PlacementGravityProperty); } + set { SetValue(PlacementGravityProperty, value); } + } + + /// + /// Gets or sets the placement mode of the context menu in relation to the. + /// + public PlacementMode PlacementMode + { + get { return GetValue(PlacementModeProperty); } + set { SetValue(PlacementModeProperty, value); } + } + + /// + /// Gets or sets the the anchor rectangle within the parent that the popup will be placed + /// relative to when is . + /// + /// + /// The placement rect defines a rectangle relative to around + /// which the popup will be opened, with determining which edge + /// of the placement target is used. + /// + /// If unset, the anchor rectangle will be the bounds of the . + /// + public Rect? PlacementRect + { + get { return GetValue(PlacementRectProperty); } + set { SetValue(PlacementRectProperty, value); } + } + /// /// Gets or sets the control that is used to determine the popup's position. /// @@ -187,7 +244,10 @@ namespace Avalonia.Controls { HorizontalOffset = HorizontalOffset, VerticalOffset = VerticalOffset, + PlacementAnchor = PlacementAnchor, + PlacementGravity = PlacementGravity, PlacementMode = PlacementMode, + PlacementRect = PlacementRect, PlacementTarget = PlacementTarget ?? control, StaysOpen = false }; From 964f0b85d44c64231e3eabbee8db5180dda64be2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 May 2020 16:04:51 +0200 Subject: [PATCH 007/108] Tweak doc comments. --- src/Avalonia.Controls/ContextMenu.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 286cb6d6d8..456af7bdde 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -106,7 +106,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the Vertical offset of the popup in relation to the . + /// Gets or sets the Vertical offset of the context menu in relation to the . /// public double VerticalOffset { @@ -144,7 +144,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the the anchor rectangle within the parent that the popup will be placed + /// Gets or sets the the anchor rectangle within the parent that the context menu will be placed /// relative to when is . /// /// From 9391ddbab5285d0d555f714692bd00628c07db9b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 22 May 2020 10:33:58 +0200 Subject: [PATCH 008/108] 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 009/108] 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 010/108] 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 011/108] 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 012/108] 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 013/108] 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 bd6d61c1724f16eae22ed8c117b17ec27923f19e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 13:06:36 -0300 Subject: [PATCH 014/108] initial test of styling menus and context menus. --- src/Avalonia.Themes.Fluent/ContextMenu.xaml | 37 +++++++++++---------- src/Avalonia.Themes.Fluent/Menu.xaml | 2 +- src/Avalonia.Themes.Fluent/MenuItem.xaml | 37 ++++++++++++++++----- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/ContextMenu.xaml index 75f8f7c23d..9a583e2e30 100644 --- a/src/Avalonia.Themes.Fluent/ContextMenu.xaml +++ b/src/Avalonia.Themes.Fluent/ContextMenu.xaml @@ -1,22 +1,25 @@ \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/MenuItem.xaml b/src/Avalonia.Themes.Fluent/MenuItem.xaml index 314416cda0..d2586c6aa8 100644 --- a/src/Avalonia.Themes.Fluent/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/MenuItem.xaml @@ -2,13 +2,23 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls" xmlns:sys="clr-namespace:System;assembly=netstandard"> + + + + + + + + + + 12,9,12,12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 020/108] 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 7b1a9fbb84e14124a67d54748571344f6db74bc4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jun 2020 15:26:10 +0200 Subject: [PATCH 021/108] Fix caret not displaying when focused via Tab key. Also set caret position to end when selecting all, as textboxes when selected should scroll to display the end of the text. --- src/Avalonia.Controls/TextBox.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 42e16e6979..c2f496287c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -347,7 +347,7 @@ namespace Avalonia.Controls if (IsFocused) { - DecideCaretVisibility(); + _presenter?.ShowCaret(); } } @@ -364,14 +364,7 @@ namespace Avalonia.Controls { SelectAll(); } - else - { - DecideCaretVisibility(); - } - } - private void DecideCaretVisibility() - { _presenter?.ShowCaret(); } @@ -975,6 +968,7 @@ namespace Avalonia.Controls { SelectionStart = 0; SelectionEnd = Text?.Length ?? 0; + CaretIndex = SelectionEnd; } private bool DeleteSelection() From c2ad34a4ce072ead90aa23323b76544a6fd91a9b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jun 2020 17:14:43 +0200 Subject: [PATCH 022/108] Fix moving caret left/right when selection present. When a selection is present, pressing the left key should move the caret to the start of the selection and pressing the right key should move the caret to the end of the selection. --- src/Avalonia.Controls/TextBox.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index c2f496287c..394699ce64 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -573,15 +573,15 @@ namespace Avalonia.Controls switch (e.Key) { case Key.Left: - MoveHorizontal(-1, hasWholeWordModifiers); - movement = true; selection = DetectSelection(); + MoveHorizontal(-1, hasWholeWordModifiers, selection); + movement = true; break; case Key.Right: - MoveHorizontal(1, hasWholeWordModifiers); - movement = true; selection = DetectSelection(); + MoveHorizontal(1, hasWholeWordModifiers, selection); + movement = true; break; case Key.Up: @@ -826,13 +826,21 @@ namespace Avalonia.Controls return result; } - private void MoveHorizontal(int direction, bool wholeWord) + private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) { var text = Text ?? string.Empty; var caretIndex = CaretIndex; if (!wholeWord) { + if (SelectionStart != SelectionEnd && !isSelecting) + { + var start = Math.Min(SelectionStart, SelectionEnd); + var end = Math.Max(SelectionStart, SelectionEnd); + CaretIndex = direction < 0 ? start : end; + return; + } + var index = caretIndex + direction; if (index < 0 || index > text.Length) @@ -1049,14 +1057,14 @@ namespace Avalonia.Controls private void SetSelectionForControlBackspace() { SelectionStart = CaretIndex; - MoveHorizontal(-1, true); + MoveHorizontal(-1, true, false); SelectionEnd = CaretIndex; } private void SetSelectionForControlDelete() { SelectionStart = CaretIndex; - MoveHorizontal(1, true); + MoveHorizontal(1, true, false); SelectionEnd = CaretIndex; } 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 023/108] 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 033/108] 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 034/108] 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 035/108] 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 036/108] 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 037/108] 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 038/108] 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 039/108] 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 040/108] 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 041/108] 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 042/108] 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 043/108] 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 044/108] 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 045/108] 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 046/108] 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 047/108] 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 048/108] 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. +