From c35ea02e423152c8d717cfc12d3407125be1f3c7 Mon Sep 17 00:00:00 2001 From: OliverMiracle Date: Thu, 15 Aug 2024 11:17:39 +0200 Subject: [PATCH 1/4] Adds CalculateDimension function to properly deal with dimension calculation Adds the unit tests to check whether children has proper dimensions --- .../Controls/ProportionalStackPanel.cs | 28 ++- .../Controls/ProportionalStackPanelTests.cs | 174 ++++++++++-------- 2 files changed, 119 insertions(+), 83 deletions(-) diff --git a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs index dd8df8743..49d18038c 100644 --- a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs +++ b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs @@ -255,14 +255,14 @@ 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, i); 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, i); var size = constraint.WithHeight(height); control.Measure(size); break; @@ -299,7 +299,7 @@ protected override Size MeasureOverride(Size constraint) } else { - usedWidth += Math.Max(0, (constraint.Width - splitterThickness) * proportion); + usedWidth += CalculateDimension(constraint.Width - splitterThickness, proportion, i); } break; @@ -314,7 +314,7 @@ protected override Size MeasureOverride(Size constraint) } else { - usedHeight += Math.Max(0, (constraint.Height - splitterThickness) * proportion); + usedHeight += CalculateDimension(constraint.Height - splitterThickness, proportion, i); } break; @@ -397,7 +397,7 @@ 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, i); remainingRect = remainingRect.WithWidth(width); left += width; } @@ -414,7 +414,7 @@ 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, i); remainingRect = remainingRect.WithHeight(height); top += height; } @@ -430,6 +430,22 @@ protected override Size ArrangeOverride(Size arrangeSize) return arrangeSize; } + + private double CalculateDimension(double dimension, double proportion, int childIndex) + { + var childDimension = dimension * proportion; + var flooredChildDimension = Math.Floor(childDimension); + + // checks whether division doesn't leave a fraction + if (childDimension == flooredChildDimension) + return Math.Max(0, flooredChildDimension); + else + { + // if so, it assigns the divided pixel to the first control in proportional split + int isFirst = childIndex == 0 ? 1 : 0; + return Math.Max(0, flooredChildDimension + isFirst); + } + } /// 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..e306acf2a 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, 189, 411]; + } + + [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 } } }, @@ -163,49 +202,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 } } } } From d454fe197d0e0322200edac801b2608976ce513b Mon Sep 17 00:00:00 2001 From: OliverMiracle Date: Thu, 15 Aug 2024 11:18:44 +0200 Subject: [PATCH 2/4] comment edit --- src/Dock.Avalonia/Controls/ProportionalStackPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs index 49d18038c..3ea600b18 100644 --- a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs +++ b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs @@ -441,7 +441,7 @@ private double CalculateDimension(double dimension, double proportion, int child return Math.Max(0, flooredChildDimension); else { - // if so, it assigns the divided pixel to the first control in proportional split + // if it leaves, it assigns the divided pixel to the first control in proportional split int isFirst = childIndex == 0 ? 1 : 0; return Math.Max(0, flooredChildDimension + isFirst); } From a6d9f10bbc50cbd1fe62d536c1d0bcdd32ab8097 Mon Sep 17 00:00:00 2001 From: OliverMiracle Date: Thu, 15 Aug 2024 14:25:55 +0200 Subject: [PATCH 3/4] rewrite the command function to sum up the fractions of the pixels --- .../Controls/ProportionalStackPanel.cs | 60 +++++++++++-------- .../Controls/ProportionalStackPanelTests.cs | 9 +-- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs index 3ea600b18..a9437e33a 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 = CalculateDimension(constraint.Width - splitterThickness, proportion, i); + var width = CalculateDimension(constraint.Width - splitterThickness, proportion, + ref sumOfFractions); var size = constraint.WithWidth(width); control.Measure(size); break; } case Orientation.Vertical: { - var height = CalculateDimension(constraint.Height - splitterThickness, proportion, i); + 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 += CalculateDimension(constraint.Width - splitterThickness, proportion, i); + usedWidth += CalculateDimension(constraint.Width - splitterThickness, proportion, + ref sumOfFractions); } break; @@ -314,7 +319,8 @@ protected override Size MeasureOverride(Size constraint) } else { - usedHeight += CalculateDimension(constraint.Height - splitterThickness, proportion, i); + 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,7 @@ protected override Size ArrangeOverride(Size arrangeSize) else { Debug.Assert(!double.IsNaN(proportion)); - var width = CalculateDimension(arrangeSize.Width - splitterThickness, proportion, i); + var width = CalculateDimension(arrangeSize.Width - splitterThickness, proportion, ref sumOfFractions); remainingRect = remainingRect.WithWidth(width); left += width; } @@ -414,7 +421,7 @@ protected override Size ArrangeOverride(Size arrangeSize) else { Debug.Assert(!double.IsNaN(proportion)); - var height = CalculateDimension(arrangeSize.Height - splitterThickness, proportion, i); + var height = CalculateDimension(arrangeSize.Height - splitterThickness, proportion, ref sumOfFractions); remainingRect = remainingRect.WithHeight(height); top += height; } @@ -430,21 +437,26 @@ protected override Size ArrangeOverride(Size arrangeSize) return arrangeSize; } - - private double CalculateDimension(double dimension, double proportion, int childIndex) + + private double CalculateDimension( + double dimension, + double proportion, + ref double sumOfFractions) { var childDimension = dimension * proportion; var flooredChildDimension = Math.Floor(childDimension); - - // checks whether division doesn't leave a fraction - if (childDimension == flooredChildDimension) - return Math.Max(0, flooredChildDimension); - else + + // 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) { - // if it leaves, it assigns the divided pixel to the first control in proportional split - int isFirst = childIndex == 0 ? 1 : 0; - return Math.Max(0, flooredChildDimension + isFirst); + sumOfFractions -= Math.Round(sumOfFractions); + return Math.Max(0, flooredChildDimension + 1); } + + return Math.Max(0, flooredChildDimension); } /// diff --git a/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs b/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs index e306acf2a..cd705fcbb 100644 --- a/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs +++ b/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs @@ -65,7 +65,7 @@ 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, 189, 411]; + yield return [0.3141592653589793238462643383279, 604, 188, 412]; } [Theory] @@ -187,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); } From 4007ce3eb00e64687d428d22bfad37f99b57ce62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Mon, 23 Sep 2024 22:59:34 +0200 Subject: [PATCH 4/4] Update ProportionalStackPanel.cs --- .../Controls/ProportionalStackPanel.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs index a9437e33a..e1a331e1e 100644 --- a/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs +++ b/src/Dock.Avalonia/Controls/ProportionalStackPanel.cs @@ -404,7 +404,8 @@ protected override Size ArrangeOverride(Size arrangeSize) else { Debug.Assert(!double.IsNaN(proportion)); - var width = CalculateDimension(arrangeSize.Width - splitterThickness, proportion, ref sumOfFractions); + var width = CalculateDimension(arrangeSize.Width - splitterThickness, proportion, + ref sumOfFractions); remainingRect = remainingRect.WithWidth(width); left += width; } @@ -421,7 +422,8 @@ protected override Size ArrangeOverride(Size arrangeSize) else { Debug.Assert(!double.IsNaN(proportion)); - var height = CalculateDimension(arrangeSize.Height - splitterThickness, proportion, ref sumOfFractions); + var height = CalculateDimension(arrangeSize.Height - splitterThickness, proportion, + ref sumOfFractions); remainingRect = remainingRect.WithHeight(height); top += height; } @@ -439,13 +441,13 @@ protected override Size ArrangeOverride(Size arrangeSize) } private double CalculateDimension( - double dimension, - double proportion, + double dimension, + double proportion, ref double sumOfFractions) { var childDimension = dimension * proportion; var flooredChildDimension = Math.Floor(childDimension); - + // sums fractions from the division sumOfFractions += childDimension - flooredChildDimension; @@ -455,7 +457,7 @@ private double CalculateDimension( sumOfFractions -= Math.Round(sumOfFractions); return Math.Max(0, flooredChildDimension + 1); } - + return Math.Max(0, flooredChildDimension); }