Browse Source

Considering scaling when centering windows (#16158)

* removed duplicated code between Window.Show and Window.ShowDialog

* Handling different cases of window initial position and size + unit test

* positioning cursor on resize grip in WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip test

* Fix for flaky test

* displaying decimal digits of slider value to avoid some issues with rounding

---------

Co-authored-by: Herman Kirshin <[email protected]>
Herman K. 1 year ago
parent
commit
7a42c7eb8f

+ 162 - 103
src/Avalonia.Controls/Window.cs

@@ -94,6 +94,7 @@ namespace Avalonia.Controls
         private Thickness _windowDecorationMargin;
         private Thickness _offScreenMargin;
         private bool _canHandleResized = false;
+        private Size _arrangeBounds;
 
         /// <summary>
         /// Defines the <see cref="SizeToContent"/> property.
@@ -198,6 +199,7 @@ namespace Avalonia.Controls
         private readonly Size _maxPlatformClientSize;
         private bool _shown;
         private bool _showingAsDialog;
+        private bool _positionWasSet;
         private bool _wasShownBefore;
 
         /// <summary>
@@ -428,7 +430,11 @@ namespace Avalonia.Controls
         public PixelPoint Position
         {
             get => PlatformImpl?.Position ?? PixelPoint.Origin;
-            set => PlatformImpl?.Move(value);
+            set
+            {
+                PlatformImpl?.Move(value);
+                _positionWasSet = true;
+            }
         }
 
         /// <summary>
@@ -622,7 +628,7 @@ namespace Avalonia.Controls
         /// </exception>
         public override void Show()
         {
-            ShowCore(null);
+            ShowCore<object>(null, false);
         }
 
         protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
@@ -666,7 +672,7 @@ namespace Avalonia.Controls
                 throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent.");
             }
 
-            ShowCore(owner);
+            ShowCore<object>(owner, false);
         }
 
         private void EnsureStateBeforeShow()
@@ -695,12 +701,16 @@ namespace Avalonia.Controls
             }
         }
 
-        private void ShowCore(Window? owner)
+        private Task<TResult>? ShowCore<TResult>(Window? owner, bool modal)
         {
             using (FreezeVisibilityChangeHandling())
             {
                 EnsureStateBeforeShow();
-
+                
+                if (modal && owner == null)
+                {
+                    throw new ArgumentNullException(nameof(owner));
+                }
                 if (owner != null)
                 {
                     EnsureParentStateBeforeShow(owner);
@@ -708,7 +718,9 @@ namespace Avalonia.Controls
 
                 if (_shown)
                 {
-                    return;
+                    if (modal)
+                        throw new InvalidOperationException("The window is already being shown.");
+                    return null;
                 }
 
                 RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
@@ -716,35 +728,93 @@ namespace Avalonia.Controls
                 EnsureInitialized();
                 ApplyStyling();
                 _shown = true;
+                _showingAsDialog = modal;
                 IsVisible = true;
 
-                // We need to set position first because it is required for getting correct display scale. If position is not manual then it can be
-                // determined only by calling this method. But here it will calculate not precise location because scaling may not yet be applied (see i.e. X11Window),
-                // thus we ought to call it again later to center window correctly if needed, when scaling will be already applied
-                SetWindowStartupLocation(owner);
+                // If window position was not set before then platform may provide incorrect scaling at this time,
+                // but we need it for proper calculation of position and in some cases size (size to content)
+                SetExpectedScaling(owner);
 
-                _canHandleResized = true; 
-                
                 var initialSize = new Size(
-                    double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width,
-                    double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height);
+                    double.IsNaN(Width) ? ClientSize.Width : Width,
+                    double.IsNaN(Height) ? ClientSize.Height : Height);
+                
+                initialSize = new Size(
+                MathUtilities.Clamp(initialSize.Width, MinWidth, MaxWidth),
+                MathUtilities.Clamp(initialSize.Height, MinHeight, MaxHeight));
 
-                if (initialSize != ClientSize)
+                var clientSizeChanged = initialSize != ClientSize;
+                ClientSize = initialSize; // ClientSize is required for Measure and Arrange
+                
+                // this will call ArrangeSetBounds
+                LayoutManager.ExecuteInitialLayoutPass();
+
+                if (SizeToContent.HasFlag(SizeToContent.Width))
                 {
-                    PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout);
+                    initialSize = initialSize.WithWidth(MathUtilities.Clamp(_arrangeBounds.Width, MinWidth, MaxWidth));
+                    clientSizeChanged |= initialSize != ClientSize;
+                    ClientSize = initialSize;
                 }
 
-                LayoutManager.ExecuteInitialLayoutPass();
-
+                if (SizeToContent.HasFlag(SizeToContent.Height))
+                {
+                    initialSize = initialSize.WithHeight(MathUtilities.Clamp(_arrangeBounds.Height, MinHeight, MaxHeight));
+                    clientSizeChanged |= initialSize != ClientSize;
+                    ClientSize = initialSize;
+                }
+                
                 Owner = owner;
 
-                // Second call will calculate correct position because both current and owner windows have correct scaling.
                 SetWindowStartupLocation(owner);
+                
+                DesktopScalingOverride = null;
+                
+                if (clientSizeChanged || ClientSize != PlatformImpl?.ClientSize)
+                {
+                    // Previously it was called before ExecuteInitialLayoutPass
+                    PlatformImpl?.Resize(ClientSize, WindowResizeReason.Layout);
+                    
+                    // we do not want PlatformImpl?.Resize to trigger HandleResized yet because it will set Width and Height.
+                    // So perform some important actions from HandleResized
+                    
+                    Renderer.Resized(ClientSize);
+                    OnResized(new WindowResizedEventArgs(ClientSize, WindowResizeReason.Layout));
+
+                    if (!double.IsNaN(Width))
+                        Width = ClientSize.Width;
+                    if (!double.IsNaN(Height))
+                        Height = ClientSize.Height;
+                }
 
+                FrameSize = PlatformImpl?.FrameSize;
+
+                _canHandleResized = true; 
+                
                 StartRendering();
-                PlatformImpl?.Show(ShowActivated, false);
+                PlatformImpl?.Show(ShowActivated, modal);
+
+                Task<TResult>? result = null;
+                if (modal)
+                {
+                    var tcs = new TaskCompletionSource<TResult>();
+
+                    Observable.FromEventPattern(
+                            x => Closed += x,
+                            x => Closed -= x)
+                        .Take(1)
+                        .Subscribe(_ =>
+                        {
+                            owner!.Activate();
+                            tcs.SetResult((TResult)(_dialogResult ?? default(TResult)!));
+                        });
+                    result = tcs.Task;
+                }
+
                 OnOpened(EventArgs.Empty);
-                _wasShownBefore = true;
+                if (!modal)
+                    _wasShownBefore = true;
+                
+                return result;
             }
         }
 
@@ -773,74 +843,7 @@ namespace Avalonia.Controls
         /// <returns>.
         /// A task that can be used to retrieve the result of the dialog when it closes.
         /// </returns>
-        public Task<TResult> ShowDialog<TResult>(Window owner)
-        {
-            using (FreezeVisibilityChangeHandling())
-            {
-                EnsureStateBeforeShow();
-
-                if (owner == null)
-                {
-                    throw new ArgumentNullException(nameof(owner));
-                }
-
-                EnsureParentStateBeforeShow(owner);
-
-                if (_shown)
-                {
-                    throw new InvalidOperationException("The window is already being shown.");
-                }
-
-                RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
-
-                EnsureInitialized();
-                ApplyStyling();
-                _shown = true;
-                _showingAsDialog = true;
-                IsVisible = true;
-
-                // We need to set position first because it is required for getting correct display scale. If position is not manual then it can be
-                // determined only by calling this method. But here it will calculate not precise location because scaling may not yet be applied (see i.e. X11Window),
-                // thus we ought to call it again later to center window correctly if needed, when scaling will be already applied
-                SetWindowStartupLocation(owner);
-                
-                _canHandleResized = true; 
-
-                var initialSize = new Size(
-                    double.IsNaN(Width) ? ClientSize.Width : Width,
-                    double.IsNaN(Height) ? ClientSize.Height : Height);
-
-                if (initialSize != ClientSize)
-                {
-                    PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout);
-                }
-
-                LayoutManager.ExecuteInitialLayoutPass();
-
-                var result = new TaskCompletionSource<TResult>();
-
-                Owner = owner;
-
-                // Second call will calculate correct position because both current and owner windows have correct scaling.
-                SetWindowStartupLocation(owner);
-
-                StartRendering();
-                PlatformImpl?.Show(ShowActivated, true);
-
-                Observable.FromEventPattern(
-                        x => Closed += x,
-                        x => Closed -= x)
-                    .Take(1)
-                    .Subscribe(_ =>
-                    {
-                        owner.Activate();
-                        result.SetResult((TResult)(_dialogResult ?? default(TResult)!));
-                    });
-
-                OnOpened(EventArgs.Empty);
-                return result.Task;
-            }
-        }
+        public Task<TResult> ShowDialog<TResult>(Window owner) => ShowCore<TResult>(owner, true)!;
 
         /// <summary>
         /// Sorts the windows ascending by their Z order - the topmost window will be the last in the list.
@@ -925,31 +928,67 @@ namespace Avalonia.Controls
             }
         }
 
-        private void SetWindowStartupLocation(Window? owner = null)
+        private void SetExpectedScaling(WindowBase? owner)
         {
-            if (_wasShownBefore == true)
+            if (_wasShownBefore)
             {
                 return;
             }
+            
+            var location = GetEffectiveWindowStartupLocation(owner);
 
+            switch (location)
+            {
+                case WindowStartupLocation.CenterOwner:
+                    DesktopScalingOverride = owner?.DesktopScaling;
+                    break;
+                case WindowStartupLocation.CenterScreen:
+                    DesktopScalingOverride = owner?.DesktopScaling ?? Screens.ScreenFromPoint(Position)?.Scaling ?? Screens.Primary?.Scaling;
+                    break;
+                case WindowStartupLocation.Manual:
+                    DesktopScalingOverride = Screens.ScreenFromPoint(Position)?.Scaling;
+                    break;
+            }
+        }
+
+        private WindowStartupLocation GetEffectiveWindowStartupLocation(WindowBase? owner)
+        {
             var startupLocation = WindowStartupLocation;
 
             if (startupLocation == WindowStartupLocation.CenterOwner &&
                 (owner is null ||
-                 (Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized))
-                )
+                 (owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized))
+               )
             {
                 // If startup location is CenterOwner, but owner is null or minimized then fall back
                 // to CenterScreen. This behavior is consistent with WPF.
                 startupLocation = WindowStartupLocation.CenterScreen;
             }
 
-            var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1;
+            return startupLocation;
+        }
+        
+        private void SetWindowStartupLocation(Window? owner = null)
+        {
+            if (_wasShownBefore)
+            {
+                return;
+            }
+
+            var startupLocation = GetEffectiveWindowStartupLocation(owner);
 
+            PixelRect rect;
             // Use frame size, falling back to client size if the platform can't give it to us.
-            var rect = FrameSize.HasValue ?
-                new PixelRect(PixelSize.FromSize(FrameSize.Value, scaling)) :
-                new PixelRect(PixelSize.FromSize(ClientSize, scaling));
+            if (PlatformImpl?.FrameSize.HasValue == true)
+            {
+                // Platform may calculate FrameSize with incorrect scaling, so do not trust the value.
+                var diff = PlatformImpl.FrameSize.Value - PlatformImpl.ClientSize;
+                rect = new PixelRect(PixelSize.FromSize(ClientSize + diff, DesktopScaling));
+            }
+            else
+            {
+                rect = new PixelRect(PixelSize.FromSize(ClientSize, DesktopScaling));
+            }
 
             if (startupLocation == WindowStartupLocation.CenterScreen)
             {
@@ -962,10 +1001,16 @@ namespace Avalonia.Controls
                 }
 
                 screen ??= Screens.ScreenFromPoint(Position);
-
+                screen ??= Screens.Primary;
+                
                 if (screen is not null)
                 {
-                    Position = screen.WorkingArea.CenterRect(rect).Position;
+                    var childRect = screen.WorkingArea.CenterRect(rect);
+
+                    if (Screens.ScreenFromPoint(childRect.Position) == null)
+                        childRect = ApplyScreenConstraint(screen, childRect);
+
+                    Position = childRect.Position;
                 }
             }
             else if (startupLocation == WindowStartupLocation.CenterOwner)
@@ -973,10 +1018,22 @@ namespace Avalonia.Controls
                 var ownerSize = owner!.FrameSize ?? owner.ClientSize;
                 var ownerRect = new PixelRect(
                     owner.Position,
-                    PixelSize.FromSize(ownerSize, scaling));
+                    PixelSize.FromSize(ownerSize, owner.DesktopScaling));
                 var childRect = ownerRect.CenterRect(rect);
 
-                if (Screens.ScreenFromWindow(owner)?.WorkingArea is { } constraint)
+                var screen = Screens.ScreenFromWindow(owner);
+                
+                childRect = ApplyScreenConstraint(screen, childRect);
+
+                Position = childRect.Position;
+            }
+
+            if (!_positionWasSet && DesktopScaling != PlatformImpl?.DesktopScaling) // Platform returns incorrect scaling, forcing setting position may fix it
+                PlatformImpl?.Move(Position);
+            
+            PixelRect ApplyScreenConstraint(Screen? screen, PixelRect childRect)
+            {
+                if (screen?.WorkingArea is { } constraint)
                 {
                     var maxX = constraint.Right - rect.Width;
                     var maxY = constraint.Bottom - rect.Height;
@@ -987,7 +1044,7 @@ namespace Avalonia.Controls
                         childRect = childRect.WithY(MathUtilities.Clamp(childRect.Y, constraint.Y, maxY));
                 }
 
-                Position = childRect.Position;
+                return childRect;
             }
         }
 
@@ -1048,7 +1105,9 @@ namespace Avalonia.Controls
 
         protected sealed override Size ArrangeSetBounds(Size size)
         {
-            PlatformImpl?.Resize(size, WindowResizeReason.Layout);
+            _arrangeBounds = size;
+            if (_canHandleResized)
+                PlatformImpl?.Resize(size, WindowResizeReason.Layout);
             return ClientSize;
         }
 

+ 3 - 1
src/Avalonia.Controls/WindowBase.cs

@@ -133,7 +133,9 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets the scaling factor for Window positioning and sizing.
         /// </summary>
-        public double DesktopScaling => PlatformImpl?.DesktopScaling ?? 1;
+        public double DesktopScaling => DesktopScalingOverride ?? PlatformImpl?.DesktopScaling ?? 1;
+
+        private protected double? DesktopScalingOverride { get; set; }
         
         /// <summary>
         /// Activates the window.

+ 12 - 0
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -149,6 +149,18 @@ namespace Avalonia.Win32
 
                         return IntPtr.Zero;
                     }
+                    else
+                    {
+                        // In case parent is on another screen with different scaling, window will have header scaled with
+                        // parent's scaling factor, so need to update frame
+                        SetWindowPos(hWnd,
+                            IntPtr.Zero, 0, 0, 0, 0,
+                            SetWindowPosFlags.SWP_FRAMECHANGED |
+                            SetWindowPosFlags.SWP_NOSIZE |
+                            SetWindowPosFlags.SWP_NOMOVE |
+                            SetWindowPosFlags.SWP_NOZORDER |
+                            SetWindowPosFlags.SWP_NOACTIVATE);
+                    }
                     break;
 
                 case WindowsMessage.WM_GETICON:

+ 15 - 0
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -524,6 +524,21 @@ namespace Avalonia.Win32
                     0,
                     0,
                     SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_NOZORDER);
+                
+                if (ShCoreAvailable && Win32Platform.WindowsVersion >= PlatformConstants.Windows8_1)
+                {
+                    var monitor = MonitorFromPoint(new POINT() { X = value.X, Y = value.Y },
+                        MONITOR.MONITOR_DEFAULTTONEAREST);
+
+                    if (GetDpiForMonitor(
+                            monitor,
+                            MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI,
+                            out _dpi,
+                            out _) == 0)
+                    {
+                        _scaling = _dpi / StandardDpi;
+                    }
+                }
             }
         }
 

+ 6 - 0
tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs

@@ -41,6 +41,12 @@ public abstract class PointerTestsBase
         impl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor());
         impl.Setup(r => r.PointToScreen(It.IsAny<Point>())).Returns<Point>(p => new PixelPoint((int)p.X, (int)p.Y));
         impl.Setup(r => r.PointToClient(It.IsAny<PixelPoint>())).Returns<PixelPoint>(p => new Point(p.X, p.Y));
+        
+        var screen1 = new Mock<Screen>(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);
+        var screens = new Mock<IScreenImpl>();
+        screens.Setup(x => x.ScreenFromWindow(It.IsAny<IWindowBaseImpl>())).Returns(screen1.Object);
+        impl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
+
         return impl;
     }
 

+ 5 - 0
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@@ -220,6 +220,11 @@ namespace Avalonia.Controls.UnitTests
             windowImpl.Setup(x => x.DesktopScaling).Returns(1);
             windowImpl.Setup(x => x.RenderScaling).Returns(1);
 
+            var screen1 = new Mock<Screen>(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);
+            var screens = new Mock<IScreenImpl>();
+            screens.Setup(x => x.ScreenFromWindow(It.IsAny<IWindowBaseImpl>())).Returns(screen1.Object);
+            windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
+            
             var services = TestServices.StyledWindow.With(
                 windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object));
 

+ 17 - 12
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -100,11 +100,9 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void IsVisible_Should_Be_False_After_Impl_Signals_Close()
         {
-            var windowImpl = new Mock<IWindowImpl>();
-            windowImpl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor());
+            var windowImpl = CreateImpl();
             windowImpl.SetupProperty(x => x.Closed);
             windowImpl.Setup(x => x.DesktopScaling).Returns(1);
-            windowImpl.Setup(x => x.RenderScaling).Returns(1);
 
             var services = TestServices.StyledWindow.With(
                 windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object));
@@ -273,7 +271,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var target = new Window(CreateImpl());
+                var target = new Window(CreateImpl().Object);
 
                 target.Show();
                 Assert.True(MediaContext.Instance.IsTopLevelActive(target));
@@ -286,7 +284,7 @@ namespace Avalonia.Controls.UnitTests
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var parent = new Window();
-                var target = new Window(CreateImpl());
+                var target = new Window(CreateImpl().Object);
 
                 parent.Show();
                 target.ShowDialog<object>(parent);
@@ -318,7 +316,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var target = new Window(CreateImpl());
+                var target = new Window(CreateImpl().Object);
 
                 target.Show();
                 target.Hide();
@@ -332,7 +330,7 @@ namespace Avalonia.Controls.UnitTests
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var parent = new Window();
-                var windowImpl = new Mock<IWindowImpl>();
+                var windowImpl = CreateImpl();
                 windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
                 windowImpl.SetupProperty(x => x.Closed);
                 windowImpl.Setup(x => x.DesktopScaling).Returns(1);
@@ -374,7 +372,7 @@ namespace Avalonia.Controls.UnitTests
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var parent = new Window();
-                var windowImpl = new Mock<IWindowImpl>();
+                var windowImpl = CreateImpl();
                 windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
                 windowImpl.SetupProperty(x => x.Closed);
                 windowImpl.Setup(x => x.DesktopScaling).Returns(1);
@@ -1099,11 +1097,18 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        private static IWindowImpl CreateImpl()
+        private static Mock<IWindowImpl> CreateImpl()
         {
-            var compositor = RendererMocks.CreateDummyCompositor();
-            return Mock.Of<IWindowImpl>(x => x.RenderScaling == 1 &&
-                                             x.Compositor == compositor);
+            var screen1 = new Mock<Screen>(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);
+            var screens = new Mock<IScreenImpl>();
+            screens.Setup(x => x.ScreenFromWindow(It.IsAny<IWindowBaseImpl>())).Returns(screen1.Object);
+
+            var windowImpl = new Mock<IWindowImpl>();
+            windowImpl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor());
+            windowImpl.Setup(x => x.RenderScaling).Returns(1);
+            windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
+
+            return windowImpl;
         }
 
         private class ChildControl : Control

+ 8 - 8
tests/Avalonia.IntegrationTests.Appium/SliderTests.cs

@@ -25,13 +25,13 @@ namespace Avalonia.IntegrationTests.Appium
 
             new Actions(Session).ClickAndHold(thumb).MoveByOffset(100, 0).Release().Perform();
 
-            var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture));
+            var value = double.Parse(slider.Text, CultureInfo.InvariantCulture);
             var boundValue = double.Parse(
                 Session.FindElementByAccessibilityId("HorizontalSliderValue").Text,
                 CultureInfo.InvariantCulture);
 
             Assert.True(value > 50);
-            Assert.Equal(value, boundValue);
+            Assert.True(Math.Abs(value - boundValue) < 2.0, $"Expected: {value}, Actual: {boundValue}");
 
             var currentThumbRect = thumb.Rect;
             Assert.True(currentThumbRect.Left > initialThumbRect.Left);
@@ -46,13 +46,13 @@ namespace Avalonia.IntegrationTests.Appium
 
             new Actions(Session).ClickAndHold(thumb).MoveByOffset(-100, 0).Release().Perform();
 
-            var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture));
+            var value = double.Parse(slider.Text, CultureInfo.InvariantCulture);
             var boundValue = double.Parse(
                 Session.FindElementByAccessibilityId("HorizontalSliderValue").Text,
                 CultureInfo.InvariantCulture);
 
             Assert.True(value < 50);
-            Assert.Equal(value, boundValue);
+            Assert.True(Math.Abs(value - boundValue) < 2.0, $"Expected: {value}, Actual: {boundValue}");
 
             var currentThumbRect = thumb.Rect;
             Assert.True(currentThumbRect.Left < initialThumbRect.Left);
@@ -67,13 +67,13 @@ namespace Avalonia.IntegrationTests.Appium
 
             new Actions(Session).MoveToElementCenter(slider, 100, 0).Click().Perform();
 
-            var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture));
+            var value = double.Parse(slider.Text, CultureInfo.InvariantCulture);
             var boundValue = double.Parse(
                 Session.FindElementByAccessibilityId("HorizontalSliderValue").Text,
                 CultureInfo.InvariantCulture);
 
             Assert.True(value > 50);
-            Assert.Equal(value, boundValue);
+            Assert.True(Math.Abs(value - boundValue) < 2.0, $"Expected: {value}, Actual: {boundValue}");
 
             var currentThumbRect = thumb.Rect;
             Assert.True(currentThumbRect.Left > initialThumbRect.Left);
@@ -88,13 +88,13 @@ namespace Avalonia.IntegrationTests.Appium
 
             new Actions(Session).MoveToElementCenter(slider, -100, 0).Click().Perform();
 
-            var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture));
+            var value = double.Parse(slider.Text, CultureInfo.InvariantCulture);
             var boundValue = double.Parse(
                 Session.FindElementByAccessibilityId("HorizontalSliderValue").Text,
                 CultureInfo.InvariantCulture);
 
             Assert.True(value < 50);
-            Assert.Equal(value, boundValue);
+            Assert.True(Math.Abs(value - boundValue) < 2.0, $"Expected: {value}, Actual: {boundValue}");
 
             var currentThumbRect = thumb.Rect;
             Assert.True(currentThumbRect.Left < initialThumbRect.Left);

+ 6 - 2
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@@ -27,6 +27,8 @@ namespace Avalonia.IntegrationTests.Appium
 
                 var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
 
+                Thread.Sleep(300); // sync with timer
+
                 Assert.Equal(1, secondaryWindowIndex);
             }
         }
@@ -38,15 +40,17 @@ namespace Avalonia.IntegrationTests.Appium
 
             using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual))
             {
+                var childWindow = GetWindow("SecondaryWindow");
+
                 new Actions(Session)
-                    .MoveToElement(mainWindow, 100, 1)
+                    .MoveToElement(childWindow, 100, 1)
                     .ClickAndHold()
                     .Perform();
 
                 var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
 
                 new Actions(Session)
-                    .MoveToElement(mainWindow, 100, 1)
+                    .MoveToElement(childWindow, 100, 1)
                     .Release()
                     .Perform();
 

+ 5 - 0
tests/Avalonia.LeakTests/ControlTests.cs

@@ -463,12 +463,17 @@ namespace Avalonia.LeakTests
         {
             using (Start())
             {
+                var screen1 = new Mock<Screen>(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);
+                var screens = new Mock<IScreenImpl>();
+                screens.Setup(x => x.ScreenFromWindow(It.IsAny<IWindowBaseImpl>())).Returns(screen1.Object);
+
                 var impl = new Mock<IWindowImpl>();
                 impl.Setup(r => r.TryGetFeature(It.IsAny<Type>())).Returns(null);
                 impl.SetupGet(x => x.RenderScaling).Returns(1);
                 impl.SetupProperty(x => x.Closed);
                 impl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
                 impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed());
+                impl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screens.Object);
 
                 AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatform>()
                     .ToConstant(new MockWindowingPlatform(() => impl.Object));