diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 4546a1aadb..788fe03162 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -117,6 +117,33 @@ namespace Avalonia.Controls.Primitives }); } + protected override Size MeasureOverride(Size availableSize) + { + var measured = base.MeasureOverride(availableSize); + var width = measured.Width; + var height = measured.Height; + var widthCache = Width; + var heightCache = Height; + + if (!double.IsNaN(widthCache)) + { + width = widthCache; + } + + width = Math.Min(width, MaxWidth); + width = Math.Max(width, MinWidth); + + if (!double.IsNaN(heightCache)) + { + height = heightCache; + } + + height = Math.Min(height, MaxHeight); + height = Math.Max(height, MinHeight); + + return new Size(width, height); + } + protected override sealed Size ArrangeSetBounds(Size size) { using (BeginAutoSizing()) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dcf4e98528..b21f18a59d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -570,8 +570,8 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size availableSize) { var sizeToContent = SizeToContent; + var constraint = availableSize; var clientSize = ClientSize; - var constraint = clientSize; if (sizeToContent.HasFlagCustom(SizeToContent.Width)) { @@ -587,12 +587,26 @@ namespace Avalonia.Controls if (!sizeToContent.HasFlagCustom(SizeToContent.Width)) { - result = result.WithWidth(clientSize.Width); + if (!double.IsInfinity(availableSize.Width)) + { + result = result.WithWidth(availableSize.Width); + } + else + { + result = result.WithWidth(clientSize.Width); + } } if (!sizeToContent.HasFlagCustom(SizeToContent.Height)) { - result = result.WithHeight(clientSize.Height); + if (!double.IsInfinity(availableSize.Height)) + { + result = result.WithHeight(availableSize.Height); + } + else + { + result = result.WithHeight(clientSize.Height); + } } return result; @@ -622,6 +636,9 @@ namespace Avalonia.Controls SizeToContent = SizeToContent.Manual; } + Width = clientSize.Width; + Height = clientSize.Height; + base.HandleResized(clientSize); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 025dfde610..bb63d1b353 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -224,8 +224,6 @@ namespace Avalonia.Controls /// The new client size. protected override void HandleResized(Size clientSize) { - Width = clientSize.Width; - Height = clientSize.Height; ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); @@ -246,17 +244,7 @@ namespace Avalonia.Controls ApplyStyling(); ApplyTemplate(); - var constraint = availableSize; - - if (!double.IsNaN(Width)) - { - constraint = constraint.WithWidth(Width); - } - - if (!double.IsNaN(Height)) - { - constraint = constraint.WithHeight(Height); - } + var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize); return MeasureOverride(constraint); } diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index af7d8ee52e..2b61de00a7 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Layout @@ -19,16 +20,11 @@ namespace Avalonia.Layout /// The control's size. public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints) { - var controlWidth = control.Width; - var controlHeight = control.Height; - - double width = (controlWidth > 0) ? controlWidth : constraints.Width; - double height = (controlHeight > 0) ? controlHeight : constraints.Height; - width = Math.Min(width, control.MaxWidth); - width = Math.Max(width, control.MinWidth); - height = Math.Min(height, control.MaxHeight); - height = Math.Max(height, control.MinHeight); - return new Size(width, height); + var minmax = new MinMax(control); + + return new Size( + MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth), + MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight)); } public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding, @@ -85,5 +81,39 @@ namespace Avalonia.Layout InnerInvalidateMeasure(control); } + + /// + /// Calculates the min and max height for a control. Ported from WPF. + /// + private readonly struct MinMax + { + public MinMax(ILayoutable e) + { + MaxHeight = e.MaxHeight; + MinHeight = e.MinHeight; + double l = e.Height; + + double height = (double.IsNaN(l) ? double.PositiveInfinity : l); + MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight); + + height = (double.IsNaN(l) ? 0 : l); + MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight); + + MaxWidth = e.MaxWidth; + MinWidth = e.MinWidth; + l = e.Width; + + double width = (double.IsNaN(l) ? double.PositiveInfinity : l); + MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth); + + width = (double.IsNaN(l) ? 0 : l); + MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth); + } + + public double MinWidth { get; } + public double MaxWidth { get; } + public double MinHeight { get; } + public double MaxHeight { get; } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index b03f8b8892..a61d313c8c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -207,6 +209,24 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Child_Should_Be_Measured_With_MaxWidth_MaxHeight_When_Set() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var window = new Window(); + var target = CreateTarget(window); + + target.MaxWidth = 500; + target.MaxHeight = 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() { @@ -216,12 +236,10 @@ namespace Avalonia.Controls.UnitTests.Primitives 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, + Height = 1344, }; var target = CreateTarget(window, popupImpl.Object); @@ -229,7 +247,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Show(); - Assert.Equal(new Size(400, 480), target.Bounds.Size); + Assert.Equal(new Size(400, 1024), 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. @@ -237,6 +255,61 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void MinWidth_MinHeight_Should_Be_Respected() + { + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); + + var target = CreateTarget(window, popupImpl.Object); + target.MinWidth = 400; + target.MinHeight = 800; + target.Content = new Border + { + Width = 100, + Height = 100, + }; + + target.Show(); + + Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds); + Assert.Equal(new Size(400, 800), target.ClientSize); + Assert.Equal(new Size(400, 800), target.PlatformImpl.ClientSize); + } + } + + [Fact] + public void Setting_Width_Should_Resize_WindowImpl() + { + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); + var positioner = new Mock(); + popupImpl.Setup(x => x.PopupPositioner).Returns(positioner.Object); + + var target = CreateTarget(window, popupImpl.Object); + target.Width = 400; + target.Height = 800; + + target.Show(); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); + + positioner.Verify(x => + x.Update(It.Is(x => x.Size.Width == 410))); + Assert.Equal(410, target.Width); + } + } + private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null) { impl ??= popupParent.PlatformImpl.CreatePopup(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 5382e6ea3e..1ac4d7a236 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -514,6 +514,32 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_Width_Should_Resize_WindowImpl() + { + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + Width = 400, + Height = 800, + }; + + target.Show(); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); + + var windowImpl = Mock.Get(target.PlatformImpl); + windowImpl.Verify(x => x.Resize(new Size(410, 800))); + Assert.Equal(410, target.Width); + } + } + private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index b8b7512c9e..b3e4b4edbc 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -66,15 +66,19 @@ namespace Avalonia.UnitTests public static Mock CreatePopupMock(IWindowBaseImpl parent) { var popupImpl = new Mock(); + var clientSize = new Size(); var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) => { + clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(size); + popupImpl.Object.Resized?.Invoke(clientSize); }); var positioner = new ManagedPopupPositioner(positionerHelper); + popupImpl.SetupAllProperties(); + popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize); popupImpl.Setup(x => x.Scaling).Returns(1); popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);