Przeglądaj źródła

Merge branch 'master' into fixes/3760-popup-swallow-click

Steven Kirk 5 lat temu
rodzic
commit
c04c791a42

+ 3 - 9
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -117,20 +117,14 @@ namespace Avalonia.Controls.Primitives
             });
         }
 
-        /// <summary>
-        /// Carries out the arrange pass of the window.
-        /// </summary>
-        /// <param name="finalSize">The final window size.</param>
-        /// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
-        protected override Size ArrangeOverride(Size finalSize)
+        protected override sealed Size ArrangeSetBounds(Size size)
         {
             using (BeginAutoSizing())
             {
-                _positionerParameters.Size = finalSize;
+                _positionerParameters.Size = size;
                 UpdatePosition();
+                return ClientSize;
             }
-
-            return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
         }
     }
 }

+ 27 - 21
src/Avalonia.Controls/Window.cs

@@ -313,22 +313,7 @@ namespace Avalonia.Controls
         /// Should be called from left mouse button press event handler
         /// </summary>
         public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
-        
-        /// <summary>
-        /// Carries out the arrange pass of the window.
-        /// </summary>
-        /// <param name="finalSize">The final window size.</param>
-        /// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
-        protected override Size ArrangeOverride(Size finalSize)
-        {
-            using (BeginAutoSizing())
-            {
-                PlatformImpl?.Resize(finalSize);
-            }
 
-            return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
-        }
-        
         /// <inheritdoc/>
         Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
 
@@ -450,6 +435,19 @@ namespace Avalonia.Controls
 
             EnsureInitialized();
             IsVisible = true;
+
+            var initialSize = new Size(
+                double.IsNaN(Width) ? ClientSize.Width : Width,
+                double.IsNaN(Height) ? ClientSize.Height : Height);
+
+            if (initialSize != ClientSize)
+            {
+                using (BeginAutoSizing())
+                {
+                    PlatformImpl?.Resize(initialSize);
+                }
+            }
+
             LayoutManager.ExecuteInitialLayoutPass(this);
 
             using (BeginAutoSizing())
@@ -569,31 +567,30 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
             var sizeToContent = SizeToContent;
             var clientSize = ClientSize;
-            var constraint = availableSize;
+            var constraint = clientSize;
 
-            if ((sizeToContent & SizeToContent.Width) != 0)
+            if (sizeToContent.HasFlagCustom(SizeToContent.Width))
             {
                 constraint = constraint.WithWidth(double.PositiveInfinity);
             }
 
-            if ((sizeToContent & SizeToContent.Height) != 0)
+            if (sizeToContent.HasFlagCustom(SizeToContent.Height))
             {
                 constraint = constraint.WithHeight(double.PositiveInfinity);
             }
 
             var result = base.MeasureOverride(constraint);
 
-            if ((sizeToContent & SizeToContent.Width) == 0)
+            if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
             {
                 result = result.WithWidth(clientSize.Width);
             }
 
-            if ((sizeToContent & SizeToContent.Height) == 0)
+            if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
             {
                 result = result.WithHeight(clientSize.Height);
             }
@@ -601,6 +598,15 @@ namespace Avalonia.Controls
             return result;
         }
 
+        protected sealed override Size ArrangeSetBounds(Size size)
+        {
+            using (BeginAutoSizing())
+            {
+                PlatformImpl?.Resize(size);
+                return ClientSize;
+            }
+        }
+
         protected sealed override void HandleClosed()
         {
             RaiseEvent(new RoutedEventArgs(WindowClosedEvent));

+ 55 - 5
src/Avalonia.Controls/WindowBase.cs

@@ -224,16 +224,66 @@ namespace Avalonia.Controls
         /// <param name="clientSize">The new client size.</param>
         protected override void HandleResized(Size clientSize)
         {
-            if (!AutoSizing)
-            {
-                Width = clientSize.Width;
-                Height = clientSize.Height;
-            }
+            Width = clientSize.Width;
+            Height = clientSize.Height;
             ClientSize = clientSize;
             LayoutManager.ExecuteLayoutPass();
             Renderer?.Resized(clientSize);
         }
 
+        /// <summary>
+        /// Overrides the core measure logic for windows.
+        /// </summary>
+        /// <param name="availableSize">The available size.</param>
+        /// <returns>The measured size.</returns>
+        /// <remarks>
+        /// The layout logic for top-level windows is different than for other controls because
+        /// they don't have a parent, meaning that many layout properties handled by the default
+        /// MeasureCore (such as margins and alignment) make no sense.
+        /// </remarks>
+        protected override Size MeasureCore(Size availableSize)
+        {
+            ApplyStyling();
+            ApplyTemplate();
+
+            var constraint = availableSize;
+
+            if (!double.IsNaN(Width))
+            {
+                constraint = constraint.WithWidth(Width);
+            }
+
+            if (!double.IsNaN(Height))
+            {
+                constraint = constraint.WithHeight(Height);
+            }
+
+            return MeasureOverride(constraint);
+        }
+
+        /// <summary>
+        /// Overrides the core arrange logic for windows.
+        /// </summary>
+        /// <param name="finalRect">The final arrange rect.</param>
+        /// <remarks>
+        /// The layout logic for top-level windows is different than for other controls because
+        /// they don't have a parent, meaning that many layout properties handled by the default
+        /// ArrangeCore (such as margins and alignment) make no sense.
+        /// </remarks>
+        protected override void ArrangeCore(Rect finalRect)
+        {
+            var constraint = ArrangeSetBounds(finalRect.Size);
+            var arrangeSize = ArrangeOverride(constraint);
+            Bounds = new Rect(arrangeSize);
+        }
+
+        /// <summary>
+        /// Called durung the arrange pass to set the size of the window.
+        /// </summary>
+        /// <param name="size">The requested size of the window.</param>
+        /// <returns>The actual size of the window.</returns>
+        protected virtual Size ArrangeSetBounds(Size size) => size;
+
         /// <summary>
         /// Handles a window position change notification from 
         /// <see cref="IWindowBaseImpl.PositionChanged"/>.

+ 4 - 3
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -209,16 +209,17 @@ namespace Avalonia.Controls.UnitTests
             screenImpl.Setup(x => x.ScreenCount).Returns(1);
             screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) });
 
-            popupImpl = MockWindowingPlatform.CreatePopupMock();
+            var windowImpl = MockWindowingPlatform.CreateWindowMock();
+            popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object);
             popupImpl.SetupGet(x => x.Scaling).Returns(1);
+            windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
 
-            var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
             windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
 
             var services = TestServices.StyledWindow.With(
                                         inputManager: new InputManager(),
                                         windowImpl: windowImpl.Object,
-                                        windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object));
+                                        windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object));
 
             return UnitTestApplication.Start(services);
         }

+ 80 - 2
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.LogicalTree;
+using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
@@ -172,9 +173,75 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
-        private PopupRoot CreateTarget(TopLevel popupParent)
+        [Fact]
+        public void Child_Should_Be_Measured_With_Infinity()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var window = new Window();
+                var target = CreateTarget(window);
+                
+                target.Content = child;
+                target.Show();
+
+                Assert.Equal(Size.Infinity, child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Child_Should_Be_Measured_With_Width_Height_When_Set()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var window = new Window();
+                var target = CreateTarget(window);
+
+                target.Width = 500;
+                target.Height = 600;
+                target.Content = child;
+                target.Show();
+
+                Assert.Equal(new Size(500, 600), child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
+        {
+            // Issue #3784.
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
+
+                popupImpl.Setup(x => x.ClientSize).Returns(new Size(400, 480));
+
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = CreateTarget(window, popupImpl.Object);
+                target.Content = child;
+
+                target.Show();
+
+                Assert.Equal(new Size(400, 480), target.Bounds.Size);
+
+                // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
+                // parent control to be offset against.
+                Assert.Equal(new Point(0, 0), target.Bounds.Position);
+            }
+        }
+
+        private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null)
         {
-            var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup())
+            impl ??= popupParent.PlatformImpl.CreatePopup();
+
+            var result = new PopupRoot(popupParent, impl)
             {
                 Template = new FuncControlTemplate<PopupRoot>((parent, scope) =>
                     new ContentPresenter
@@ -217,5 +284,16 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Popup = (Popup)this.GetVisualChildren().Single();
             }
         }
+
+        private class ChildControl : Control
+        {
+            public Size MeasureSize { get; private set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureSize = availableSize;
+                return base.MeasureOverride(availableSize);
+            }
+        }
     }
 }

+ 2 - 2
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -398,11 +398,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
                 new MockWindowingPlatform(null,
-                    () =>
+                    x =>
                     {
                         if(UsePopupHost)
                             return null;
-                        return MockWindowingPlatform.CreatePopupMock().Object;
+                        return MockWindowingPlatform.CreatePopupMock(x).Object;
                     })));
         }
 

+ 139 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
+using Avalonia.Layout;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
@@ -355,6 +356,27 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
+
+                var child = new ChildControl();
+                var target = new Window(windowImpl.Object)
+                {
+                    SizeToContent = SizeToContent.Manual,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(new Size(550, 450), child.MeasureSize);
+            }
+        }
+
         [Fact]
         public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
         {
@@ -375,6 +397,123 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
+        {
+            // Issue #3784.
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                var clientSize = new Size(200, 200);
+                var maxClientSize = new Size(480, 480);
+
+                windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
+                {
+                    clientSize = size.Constrain(maxClientSize);
+                    windowImpl.Object.Resized?.Invoke(clientSize);
+                });
+
+                windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+                var target = new Window(windowImpl.Object)
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(new Size(400, 480), target.Bounds.Size);
+
+                // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
+                // parent control to be offset against.
+                Assert.Equal(new Point(0, 0), target.Bounds.Position);
+            }
+        }
+
+        [Fact]
+        public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = new Window()
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(400, target.Width);
+                Assert.Equal(800, target.Height);
+            }
+        }
+
+        [Fact]
+        public void SizeToContent_Should_Not_Be_Lost_On_Show()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = new Window()
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+            }
+        }
+
+        [Fact]
+        public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Canvas
+                {
+                    Width = 400,
+                    Height = 800,
+                };
+
+                var target = new Window()
+                {
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(400, target.Width);
+                Assert.Equal(800, target.Height);
+
+                child.Width = 410;
+                target.LayoutManager.ExecuteLayoutPass();
+
+                Assert.Equal(410, target.Width);
+                Assert.Equal(800, target.Height);
+                Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+            }
+        }
+
         private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
         {
             return Mock.Of<IWindowImpl>(x =>

+ 3 - 105
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@@ -1,25 +1,12 @@
-using System.Diagnostics;
-using System.IO;
 using System.Linq;
-using Moq;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
-using Avalonia.Diagnostics;
-using Avalonia.Input;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.Shared.PlatformSupport;
 using Avalonia.Styling;
-using Avalonia.Themes.Default;
+using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
-using Avalonia.Media;
-using System;
-using System.Collections.Generic;
-using Avalonia.Controls.UnitTests;
-using Avalonia.UnitTests;
 
 namespace Avalonia.Layout.UnitTests
 {
@@ -28,10 +15,8 @@ namespace Avalonia.Layout.UnitTests
         [Fact]
         public void Grandchild_Size_Changed()
         {
-            using (var context = AvaloniaLocator.EnterScope())
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                RegisterServices();
-
                 Border border;
                 TextBlock textBlock;
 
@@ -55,7 +40,6 @@ namespace Avalonia.Layout.UnitTests
                 };
 
                 window.Show();
-                window.LayoutManager.ExecuteInitialLayoutPass(window);
 
                 Assert.Equal(new Size(400, 400), border.Bounds.Size);
                 textBlock.Width = 200;
@@ -68,10 +52,8 @@ namespace Avalonia.Layout.UnitTests
         [Fact]
         public void Test_ScrollViewer_With_TextBlock()
         {
-            using (var context = AvaloniaLocator.EnterScope())
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                RegisterServices();
-
                 ScrollViewer scrollViewer;
                 TextBlock textBlock;
 
@@ -79,7 +61,6 @@ namespace Avalonia.Layout.UnitTests
                 {
                     Width = 800,
                     Height = 600,
-                    SizeToContent = SizeToContent.WidthAndHeight,
                     Content = scrollViewer = new ScrollViewer
                     {
                         Width = 200,
@@ -99,7 +80,6 @@ namespace Avalonia.Layout.UnitTests
                 window.Resources["ScrollBarThickness"] = 10.0;
 
                 window.Show();
-                window.LayoutManager.ExecuteInitialLayoutPass(window);
 
                 Assert.Equal(new Size(800, 600), window.Bounds.Size);
                 Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size);
@@ -131,87 +111,5 @@ namespace Avalonia.Layout.UnitTests
         {
             return v.Bounds.Position;
         }
-
-        class FormattedTextMock : IFormattedTextImpl
-        {
-            public FormattedTextMock(string text)
-            {
-                Text = text;
-            }
-
-            public Size Constraint { get; set; }
-
-            public string Text { get; }
-
-            public Rect Bounds => Rect.Empty;
-
-            public void Dispose()
-            {
-            }
-
-            public IEnumerable<FormattedTextLine> GetLines() => new FormattedTextLine[0];
-
-            public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
-
-            public Rect HitTestTextPosition(int index) => new Rect();
-
-            public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
-
-            public Size Measure() => Constraint;
-        }
-
-        private void RegisterServices()
-        {
-            var globalStyles = new Mock<IGlobalStyles>();
-            var globalStylesResources = globalStyles.As<IResourceNode>();
-            var outObj = (object)10;
-            globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true);
-
-            var renderInterface = new Mock<IPlatformRenderInterface>();
-            renderInterface.Setup(x =>
-                x.CreateFormattedText(
-                    It.IsAny<string>(),
-                    It.IsAny<Typeface>(),
-                    It.IsAny<double>(),
-                    It.IsAny<TextAlignment>(),
-                    It.IsAny<TextWrapping>(),
-                    It.IsAny<Size>(),
-                    It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
-                .Returns(new FormattedTextMock("TEST"));
-
-            var streamGeometry = new Mock<IStreamGeometryImpl>();
-            streamGeometry.Setup(x =>
-                    x.Open())
-                .Returns(new Mock<IStreamGeometryContextImpl>().Object);
-
-            renderInterface.Setup(x =>
-                    x.CreateStreamGeometry())
-                .Returns(streamGeometry.Object);
-
-            var windowImpl = new Mock<IWindowImpl>();
-
-            Size clientSize = default(Size);
-
-            windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize);
-            windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(s => clientSize = s);
-            windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024));
-            windowImpl.SetupGet(x => x.Scaling).Returns(1);
-
-            AvaloniaLocator.CurrentMutable
-                .Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryMock())
-                .Bind<IAssetLoader>().ToConstant(new AssetLoader())
-                .Bind<IInputManager>().ToConstant(new Mock<IInputManager>().Object)
-                .Bind<IGlobalStyles>().ToConstant(globalStyles.Object)
-                .Bind<IRuntimePlatform>().ToConstant(new AppBuilder().RuntimePlatform)
-                .Bind<IPlatformRenderInterface>().ToConstant(renderInterface.Object)
-                .Bind<IStyler>().ToConstant(new Styler())
-                .Bind<IFontManagerImpl>().ToConstant(new MockFontManagerImpl())
-                .Bind<ITextShaperImpl>().ToConstant(new MockTextShaperImpl())
-                .Bind<IWindowingPlatform>().ToConstant(new Avalonia.Controls.UnitTests.WindowingPlatformMock(() => windowImpl.Object));
-
-            var theme = new DefaultTheme();
-            globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
-            globalStyles.Setup(x => x.Styles).Returns(theme);
-        }
     }
 }

+ 82 - 29
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@@ -8,65 +8,118 @@ namespace Avalonia.UnitTests
 {
     public class MockWindowingPlatform : IWindowingPlatform
     {
+        private static readonly Size s_screenSize = new Size(1280, 1024);
         private readonly Func<IWindowImpl> _windowImpl;
-        private readonly Func<IPopupImpl> _popupImpl;
+        private readonly Func<IWindowBaseImpl, IPopupImpl> _popupImpl;
 
-        public MockWindowingPlatform(Func<IWindowImpl> windowImpl = null, Func<IPopupImpl> popupImpl = null )
+        public MockWindowingPlatform(
+            Func<IWindowImpl> windowImpl = null,
+            Func<IWindowBaseImpl, IPopupImpl> popupImpl = null )
         {
             _windowImpl = windowImpl;
             _popupImpl = popupImpl;
         }
 
-        public static Mock<IWindowImpl> CreateWindowMock(Func<IPopupImpl> popupImpl = null)
+        public static Mock<IWindowImpl> CreateWindowMock()
         {
-            var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
-            var mock = Mock.Get(win);
-            mock.Setup(x => x.Show()).Callback(() =>
+            var windowImpl = new Mock<IWindowImpl>();
+            var position = new PixelPoint();
+            var clientSize = new Size(800, 600);
+
+            windowImpl.SetupAllProperties();
+            windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+            windowImpl.Setup(x => x.Scaling).Returns(1);
+            windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
+            windowImpl.Setup(x => x.Position).Returns(() => position);
+            SetupToplevel(windowImpl);
+
+            windowImpl.Setup(x => x.CreatePopup()).Returns(() =>
             {
-                mock.Object.Activated?.Invoke();
+                return CreatePopupMock(windowImpl.Object).Object;
             });
-            mock.Setup(x => x.CreatePopup()).Returns(() =>
+
+            windowImpl.Setup(x => x.Dispose()).Callback(() =>
             {
-                if (popupImpl != null)
-                    return popupImpl();
-                return CreatePopupMock().Object;
+                windowImpl.Object.Closed?.Invoke();
+            });
+
+            windowImpl.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback<PixelPoint>(x =>
+            {
+                position = x;
+                windowImpl.Object.PositionChanged?.Invoke(x);
+            });
 
+            windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(x =>
+            {
+                clientSize = x.Constrain(s_screenSize);
+                windowImpl.Object.Resized?.Invoke(clientSize);
             });
-            mock.Setup(x => x.Dispose()).Callback(() =>
+
+            windowImpl.Setup(x => x.Show()).Callback(() =>
             {
-                mock.Object.Closed?.Invoke();
+                windowImpl.Object.Activated?.Invoke();
             });
-            PixelPoint pos = default;
-            mock.SetupGet(x => x.Position).Returns(() => pos);
-            mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
-            SetupToplevel(mock);
-            return mock;
+
+            return windowImpl;
         }
 
-        static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
+        public static Mock<IPopupImpl> CreatePopupMock(IWindowBaseImpl parent)
         {
-            mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+            var popupImpl = new Mock<IPopupImpl>();
+
+            var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) =>
+            {
+                popupImpl.Object.PositionChanged?.Invoke(pos);
+                popupImpl.Object.Resized?.Invoke(size);
+            });
+            
+            var positioner = new ManagedPopupPositioner(positionerHelper);
+
+            popupImpl.Setup(x => x.Scaling).Returns(1);
+            popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);
+            
+            SetupToplevel(popupImpl);
+            
+            return popupImpl;
         }
 
-        public static Mock<IPopupImpl> CreatePopupMock()
+        public static Mock<IScreenImpl> CreateScreenMock()
         {
-            var positioner = Mock.Of<IPopupPositioner>();
-            var popup = Mock.Of<IPopupImpl>(x => x.Scaling == 1);
-            var mock = Mock.Get(popup);
-            mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
-            SetupToplevel(mock);
-            
-            return mock;
+            var screenImpl = new Mock<IScreenImpl>();
+            var bounds = new PixelRect(0, 0, (int)s_screenSize.Width, (int)s_screenSize.Height);
+            var screen = new Screen(96, bounds, bounds, true);
+            screenImpl.Setup(x => x.AllScreens).Returns(new[] { screen });
+            screenImpl.Setup(x => x.ScreenCount).Returns(1);
+            return screenImpl;
         }
 
         public IWindowImpl CreateWindow()
         {
-            return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object;
+            if (_windowImpl is object)
+            {
+                return _windowImpl();
+            }
+            else
+            {
+                var mock = CreateWindowMock();
+
+                if (_popupImpl is object)
+                {
+                    mock.Setup(x => x.CreatePopup()).Returns(() => _popupImpl(mock.Object));
+                }
+
+                return mock.Object;
+            }
         }
 
         public IEmbeddableWindowImpl CreateEmbeddableWindow()
         {
             throw new NotImplementedException();
         }
+
+        private static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
+        {
+            mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
+        }
     }
 }