diff --git a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs index dd8df8743..e1a331e1e 100644 --- a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs +++ b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs @@ -3,7 +3,6 @@ using System.Linq; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Presenters; using Avalonia.Data; using Avalonia.Layout; @@ -33,7 +32,8 @@ public Orientation Orientation /// Defines the Proportion attached property. /// public static readonly AttachedProperty ProportionProperty = - AvaloniaProperty.RegisterAttached("Proportion", double.NaN, false, BindingMode.TwoWay); + AvaloniaProperty.RegisterAttached("Proportion", double.NaN, false, + BindingMode.TwoWay); /// /// Gets the value of the Proportion attached property on the specified control. @@ -59,7 +59,8 @@ public static void SetProportion(AvaloniaObject control, double value) /// Defines the IsCollapsed attached property. /// public static readonly AttachedProperty IsCollapsedProperty = - AvaloniaProperty.RegisterAttached("IsCollapsed", false, false, BindingMode.TwoWay); + AvaloniaProperty.RegisterAttached("IsCollapsed", false, false, + BindingMode.TwoWay); /// /// Gets the value of the IsCollapsed attached property on the specified control. @@ -191,7 +192,7 @@ private double GetTotalSplitterThickness(global::Avalonia.Controls.Controls chil continue; } } - + var thickness = proportionalStackPanelSplitter.Thickness; totalThickness += thickness; } @@ -209,11 +210,11 @@ protected override Size MeasureOverride(Size constraint) { var horizontal = Orientation == Orientation.Horizontal; - if (constraint == Size.Infinity - || (horizontal && double.IsInfinity(constraint.Width)) + if (constraint == Size.Infinity + || (horizontal && double.IsInfinity(constraint.Width)) || (!horizontal && double.IsInfinity(constraint.Height))) { - throw new Exception("Proportional StackPanel cannot be inside a control that offers infinite space."); + throw new Exception("Proportional StackPanel cannot be inside a control that offers infinite space."); } var usedWidth = 0.0; @@ -225,7 +226,8 @@ protected override Size MeasureOverride(Size constraint) AssignProportions(Children); var needsNextSplitter = false; - + double sumOfFractions = 0; + // Measure each of the Children for (var i = 0; i < Children.Count; i++) { @@ -255,14 +257,16 @@ protected override Size MeasureOverride(Size constraint) { case Orientation.Horizontal: { - var width = Math.Max(0, (constraint.Width - splitterThickness) * proportion); + var width = CalculateDimension(constraint.Width - splitterThickness, proportion, + ref sumOfFractions); var size = constraint.WithWidth(width); control.Measure(size); break; } case Orientation.Vertical: { - var height = Math.Max(0, (constraint.Height - splitterThickness) * proportion); + var height = CalculateDimension(constraint.Height - splitterThickness, proportion, + ref sumOfFractions); var size = constraint.WithHeight(height); control.Measure(size); break; @@ -299,7 +303,8 @@ protected override Size MeasureOverride(Size constraint) } else { - usedWidth += Math.Max(0, (constraint.Width - splitterThickness) * proportion); + usedWidth += CalculateDimension(constraint.Width - splitterThickness, proportion, + ref sumOfFractions); } break; @@ -314,7 +319,8 @@ protected override Size MeasureOverride(Size constraint) } else { - usedHeight += Math.Max(0, (constraint.Height - splitterThickness) * proportion); + usedHeight += CalculateDimension(constraint.Height - splitterThickness, proportion, + ref sumOfFractions); } break; @@ -343,6 +349,7 @@ protected override Size ArrangeOverride(Size arrangeSize) AssignProportions(Children); var needsNextSplitter = false; + double sumOfFractions = 0; for (var i = 0; i < Children.Count; i++) { @@ -397,7 +404,8 @@ protected override Size ArrangeOverride(Size arrangeSize) else { Debug.Assert(!double.IsNaN(proportion)); - var width = Math.Max(0, (arrangeSize.Width - splitterThickness) * proportion); + var width = CalculateDimension(arrangeSize.Width - splitterThickness, proportion, + ref sumOfFractions); remainingRect = remainingRect.WithWidth(width); left += width; } @@ -414,7 +422,8 @@ protected override Size ArrangeOverride(Size arrangeSize) else { Debug.Assert(!double.IsNaN(proportion)); - var height = Math.Max(0, (arrangeSize.Height - splitterThickness) * proportion); + var height = CalculateDimension(arrangeSize.Height - splitterThickness, proportion, + ref sumOfFractions); remainingRect = remainingRect.WithHeight(height); top += height; } @@ -431,6 +440,27 @@ protected override Size ArrangeOverride(Size arrangeSize) return arrangeSize; } + private double CalculateDimension( + double dimension, + double proportion, + ref double sumOfFractions) + { + var childDimension = dimension * proportion; + var flooredChildDimension = Math.Floor(childDimension); + + // sums fractions from the division + sumOfFractions += childDimension - flooredChildDimension; + + // if the sum of fractions made up a whole pixel/pixels, add it to the dimension + if (Math.Round(sumOfFractions, 1) - Math.Clamp(Math.Floor(sumOfFractions), 1, double.MaxValue) >= 0) + { + sumOfFractions -= Math.Round(sumOfFractions); + return Math.Max(0, flooredChildDimension + 1); + } + + return Math.Max(0, flooredChildDimension); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs b/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs index 263f5f880..cd705fcbb 100644 --- a/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs +++ b/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; @@ -26,12 +28,7 @@ public void Lays_Out_Children_Horizontal() Width = 300, Height = 100, Orientation = Orientation.Horizontal, - Children = - { - new Border(), - new ProportionalStackPanelSplitter(), - new Border() - } + Children = { new Border(), new ProportionalStackPanelSplitter(), new Border() } }; target.Measure(Size.Infinity); @@ -51,12 +48,7 @@ public void Lays_Out_Children_Vertical() Width = 100, Height = 300, Orientation = Orientation.Vertical, - Children = - { - new Border(), - new ProportionalStackPanelSplitter(), - new Border() - } + Children = { new Border(), new ProportionalStackPanelSplitter(), new Border() } }; target.Measure(Size.Infinity); @@ -68,6 +60,76 @@ public void Lays_Out_Children_Vertical() Assert.Equal(new Rect(0, 152, 100, 148), target.Children[2].Bounds); } + private static IEnumerable GetBorderTestsData() + { + yield return [0.5, 604, 300, 300]; + yield return [0.25, 604, 150, 450]; + yield return [0.6283185307179586476925286766559, 604, 377, 223]; + yield return [0.3141592653589793238462643383279, 604, 188, 412]; + } + + [Theory] + [MemberData(nameof(GetBorderTestsData))] + public void Should_Not_Trim_Borders_Horizontal( + double proportion, + double expectedWidth, + double expectedFirstChildHeight, + double expectedSecondChildHeight) + { + var target = new ProportionalStackPanel() + { + Width = expectedWidth, + Height = 100, + Orientation = Orientation.Horizontal, + Children = + { + new Border { [ProportionalStackPanel.ProportionProperty] = proportion }, + new ProportionalStackPanelSplitter(), + new Border { [ProportionalStackPanel.ProportionProperty] = 1 - proportion } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + var width = target.Children.Sum(c => c.Bounds.Width); + + Assert.Equal(expectedFirstChildHeight, target.Children[0].Bounds.Width); + Assert.Equal(expectedSecondChildHeight, target.Children[2].Bounds.Width); + Assert.Equal(expectedWidth, width); + } + + [Theory] + [MemberData(nameof(GetBorderTestsData))] + public void Should_Not_Trim_Borders_Vertical( + double proportion, + double expectedHeight, + double expectedFirstChildHeight, + double expectedSecondChildHeight) + { + var target = new ProportionalStackPanel() + { + Width = 100, + Height = expectedHeight, + Orientation = Orientation.Vertical, + Children = + { + new Border { [ProportionalStackPanel.ProportionProperty] = proportion }, + new ProportionalStackPanelSplitter(), + new Border { [ProportionalStackPanel.ProportionProperty] = 1 - proportion } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + var height = target.Children.Sum(c => c.Bounds.Height); + + Assert.Equal(expectedFirstChildHeight, target.Children[0].Bounds.Height); + Assert.Equal(expectedSecondChildHeight, target.Children[2].Bounds.Height); + Assert.Equal(expectedHeight, height); + } + [Fact] public void Lays_Out_Children_Default() { @@ -84,19 +146,12 @@ public void Lays_Out_Children_Default() { new Border() { - Background = Brushes.Red, - [ProportionalStackPanel.ProportionProperty] = 0.5 + Background = Brushes.Red, [ProportionalStackPanel.ProportionProperty] = 0.5 }, new ProportionalStackPanelSplitter(), - new Border() - { - Background = Brushes.Green - }, + new Border() { Background = Brushes.Green }, new ProportionalStackPanelSplitter(), - new Border() - { - Background = Brushes.Blue - } + new Border() { Background = Brushes.Blue } } }, new ProportionalStackPanelSplitter(), @@ -104,20 +159,11 @@ public void Lays_Out_Children_Default() { Children = { - new Border() - { - Background = Brushes.Blue, - }, + new Border() { Background = Brushes.Blue, }, new ProportionalStackPanelSplitter(), - new Border() - { - Background = Brushes.Red - }, + new Border() { Background = Brushes.Red }, new ProportionalStackPanelSplitter(), - new Border() - { - Background=Brushes.Green - } + new Border() { Background = Brushes.Green } } }, new ProportionalStackPanelSplitter(), @@ -125,20 +171,13 @@ public void Lays_Out_Children_Default() { Children = { - new Border() - { - Background = Brushes.Green, - }, + new Border() { Background = Brushes.Green, }, new ProportionalStackPanelSplitter(), - new Border() - { - Background = Brushes.Blue - }, + new Border() { Background = Brushes.Blue }, new ProportionalStackPanelSplitter(), new Border() { - Background=Brushes.Red, - [ProportionalStackPanel.ProportionProperty] = 0.5 + Background = Brushes.Red, [ProportionalStackPanel.ProportionProperty] = 0.5 } } }, @@ -148,10 +187,11 @@ public void Lays_Out_Children_Default() target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); + // values have to add up to width/height of the parent control Assert.Equal(new Size(1000, 500), target.Bounds.Size); - Assert.Equal(new Rect(0, 0, 331, 500), target.Children[0].Bounds); - Assert.Equal(new Rect(331, 0, 4, 500), target.Children[1].Bounds); - Assert.Equal(new Rect(335, 0, 331, 500), target.Children[2].Bounds); + Assert.Equal(new Rect(0, 0, 330, 500), target.Children[0].Bounds); + Assert.Equal(new Rect(330, 0, 4, 500), target.Children[1].Bounds); + Assert.Equal(new Rect(334, 0, 331, 500), target.Children[2].Bounds); Assert.Equal(new Rect(665, 0, 4, 500), target.Children[3].Bounds); Assert.Equal(new Rect(669, 0, 331, 500), target.Children[4].Bounds); } @@ -163,49 +203,30 @@ public void Lays_Out_Children_ItemsControl() { Width = 1000, Height = 500, - ItemsPanel = new ItemsPanelTemplate() - { - Content = new ProportionalStackPanel() + ItemsPanel = + new ItemsPanelTemplate() { - Orientation = Orientation.Horizontal - } - }, + Content = new ProportionalStackPanel() { Orientation = Orientation.Horizontal } + }, ItemsSource = new List() { - new Border() - { - Background = Brushes.Green - }, + new Border() { Background = Brushes.Green }, new ProportionalStackPanelSplitter(), - new Border() - { - Background = Brushes.Blue - }, + new Border() { Background = Brushes.Blue }, new ProportionalStackPanelSplitter(), new ItemsControl() { - ItemsPanel = new ItemsPanelTemplate() - { - Content = new ProportionalStackPanel() + ItemsPanel = + new ItemsPanelTemplate() { - Orientation = Orientation.Vertical, - } - }, + Content = new ProportionalStackPanel() { Orientation = Orientation.Vertical, } + }, ItemsSource = new List() { - new Border() - { - Background = Brushes.Green - }, + new Border() { Background = Brushes.Green }, new ProportionalStackPanelSplitter(), - new Border() - { - Background = Brushes.Blue - }, - new Border() - { - Background = Brushes.Red - } + new Border() { Background = Brushes.Blue }, + new Border() { Background = Brushes.Red } } } }