From 0a68f6fbc0ac4b54d58a07b10418afca15a1e978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 2 Jul 2025 10:51:24 +0200 Subject: [PATCH 1/5] Bind tool size constraints and drop model dependency --- .../Controls/ProportionalDockControl.axaml | 6 +- .../Controls/ToolChromeControl.axaml | 4 + .../Controls/ToolContentControl.axaml | 4 + src/Dock.Avalonia/Controls/ToolControl.axaml | 4 + .../Controls/ToolDockControl.axaml | 4 + .../ProportionalStackPanel.cs | 98 ++++++++++++------- src/Dock.Model.Avalonia/Controls/Tool.cs | 57 ----------- src/Dock.Model.Avalonia/Core/DockableBase.cs | 64 ++++++++++++ src/Dock.Model.Mvvm/Controls/Tool.cs | 36 ------- src/Dock.Model.Mvvm/Core/DockableBase.cs | 36 +++++++ src/Dock.Model.ReactiveUI/Controls/Tool.cs | 36 ------- .../Core/DockableBase.cs | 20 ++++ src/Dock.Model/Controls/ITool.cs | 19 ---- src/Dock.Model/Core/IDockable.cs | 26 ++++- 14 files changed, 225 insertions(+), 189 deletions(-) diff --git a/src/Dock.Avalonia/Controls/ProportionalDockControl.axaml b/src/Dock.Avalonia/Controls/ProportionalDockControl.axaml index 6525a3333..c404d7235 100644 --- a/src/Dock.Avalonia/Controls/ProportionalDockControl.axaml +++ b/src/Dock.Avalonia/Controls/ProportionalDockControl.axaml @@ -20,7 +20,7 @@ Classes="ProportionalStackPanel"> diff --git a/src/Dock.Avalonia/Controls/ToolChromeControl.axaml b/src/Dock.Avalonia/Controls/ToolChromeControl.axaml index 7f04cd0f8..689d5e096 100644 --- a/src/Dock.Avalonia/Controls/ToolChromeControl.axaml +++ b/src/Dock.Avalonia/Controls/ToolChromeControl.axaml @@ -48,6 +48,10 @@ + + + + diff --git a/src/Dock.Avalonia/Controls/ToolContentControl.axaml b/src/Dock.Avalonia/Controls/ToolContentControl.axaml index 8d7aba83e..21c877b35 100644 --- a/src/Dock.Avalonia/Controls/ToolContentControl.axaml +++ b/src/Dock.Avalonia/Controls/ToolContentControl.axaml @@ -11,6 +11,10 @@ + + + + diff --git a/src/Dock.Avalonia/Controls/ToolControl.axaml b/src/Dock.Avalonia/Controls/ToolControl.axaml index c35df65a4..b3be96132 100644 --- a/src/Dock.Avalonia/Controls/ToolControl.axaml +++ b/src/Dock.Avalonia/Controls/ToolControl.axaml @@ -12,6 +12,10 @@ + + + + diff --git a/src/Dock.Avalonia/Controls/ToolDockControl.axaml b/src/Dock.Avalonia/Controls/ToolDockControl.axaml index 4276e8be5..524a71d3e 100644 --- a/src/Dock.Avalonia/Controls/ToolDockControl.axaml +++ b/src/Dock.Avalonia/Controls/ToolDockControl.axaml @@ -11,6 +11,10 @@ + + + + diff --git a/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs b/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs index 798f209ef..e44b2f881 100644 --- a/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs +++ b/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs @@ -87,12 +87,12 @@ public static void SetIsCollapsed(AvaloniaObject control, bool value) control.SetValue(IsCollapsedProperty, value); } - private void AssignProportions() + private void AssignProportions(Size size, double splitterThickness) { isAssigningProportions = true; try { - AssignProportionsInternal(Children); + AssignProportionsInternal(Children, size, splitterThickness, Orientation); } finally { @@ -100,53 +100,81 @@ private void AssignProportions() } } - private static void AssignProportionsInternal(Avalonia.Controls.Controls children) + private static double? GetFixedSize(Control control, Orientation orientation) { + if (orientation == Orientation.Horizontal) + { + if (!double.IsNaN(control.MinWidth)) + return control.MinWidth; + if (!double.IsNaN(control.MaxWidth)) + return control.MaxWidth; + } + else + { + if (!double.IsNaN(control.MinHeight)) + return control.MinHeight; + if (!double.IsNaN(control.MaxHeight)) + return control.MaxHeight; + } + + return null; + } + + private static void AssignProportionsInternal(Avalonia.Controls.Controls children, Size size, double splitterThickness, Orientation orientation) + { + var dimension = orientation == Orientation.Horizontal ? size.Width : size.Height; + var dimensionWithoutSplitters = Math.Max(1.0, dimension - splitterThickness); + var assignedProportion = 0.0; - var unassignedProportions = 0; + var dynamicChildren = children.OfType().Where(c => + { + var isCollapsed = GetIsCollapsed(c); + var isSplitter = ProportionalStackPanelSplitter.IsSplitter(c, out _); + return !isSplitter && !isCollapsed && GetFixedSize(c, orientation) == null; + }).ToList(); + double dynamicSum = 0.0; - for (var i = 0; i < children.Count; i++) + foreach (var control in children.OfType()) { - var control = children[i]; var isCollapsed = GetIsCollapsed(control); var isSplitter = ProportionalStackPanelSplitter.IsSplitter(control, out _); + if (isSplitter) + continue; - if (!isSplitter) + var proportion = GetProportion(control); + if (isCollapsed) { - var proportion = GetProportion(control); - - if (isCollapsed) - { - proportion = 0.0; - } + proportion = 0.0; + } + var fixedSize = GetFixedSize(control, orientation); + if (fixedSize.HasValue) + { + proportion = fixedSize.Value / dimensionWithoutSplitters; + SetProportion(control, proportion); + assignedProportion += proportion; + } + else + { if (double.IsNaN(proportion)) { - unassignedProportions++; - } - else - { - assignedProportion += proportion; + proportion = 1.0; } + dynamicSum += proportion; } } - if (unassignedProportions > 0) + if (dynamicChildren.Count > 0) { - var toAssign = assignedProportion; - foreach (var control in children.Where(c => - { - var isCollapsed = GetIsCollapsed(c); - return !isCollapsed && double.IsNaN(GetProportion(c)); - })) + var available = Math.Max(0.0, 1.0 - assignedProportion); + foreach (var control in dynamicChildren) { - if (!ProportionalStackPanelSplitter.IsSplitter(control, out _)) - { - var proportion = (1.0 - toAssign) / unassignedProportions; - SetProportion(control, proportion); - assignedProportion += (1.0 - toAssign) / unassignedProportions; - } + var p = GetProportion(control); + if (double.IsNaN(p)) p = 1.0; + var newProp = dynamicSum > 0 ? (p / dynamicSum) * available : available / dynamicChildren.Count; + SetProportion(control, newProp); } + assignedProportion = 1.0; } if (assignedProportion < 1) @@ -156,9 +184,7 @@ private static void AssignProportionsInternal(Avalonia.Controls.Controls childre var isCollapsed = GetIsCollapsed(c); return !ProportionalStackPanelSplitter.IsSplitter(c, out _) && !isCollapsed; }); - var toAdd = (1.0 - assignedProportion) / numChildren; - foreach (var child in children.Where(c => { var isCollapsed = GetIsCollapsed(c); @@ -176,9 +202,7 @@ private static void AssignProportionsInternal(Avalonia.Controls.Controls childre var isCollapsed = GetIsCollapsed(c); return !ProportionalStackPanelSplitter.IsSplitter(c, out _) && !isCollapsed; }); - var toRemove = (assignedProportion - 1.0) / numChildren; - foreach (var child in children.Where(c => { var isCollapsed = GetIsCollapsed(c); @@ -249,7 +273,7 @@ protected override Size MeasureOverride(Size constraint) var maximumHeight = 0.0; var splitterThickness = GetTotalSplitterThickness(Children); - AssignProportions(); + AssignProportions(constraint, splitterThickness); var needsNextSplitter = false; double sumOfFractions = 0; @@ -372,7 +396,7 @@ protected override Size ArrangeOverride(Size arrangeSize) var splitterThickness = GetTotalSplitterThickness(Children); var index = 0; - AssignProportions(); + AssignProportions(arrangeSize, splitterThickness); var needsNextSplitter = false; double sumOfFractions = 0; diff --git a/src/Dock.Model.Avalonia/Controls/Tool.cs b/src/Dock.Model.Avalonia/Controls/Tool.cs index 221c7c59f..86589a98f 100644 --- a/src/Dock.Model.Avalonia/Controls/Tool.cs +++ b/src/Dock.Model.Avalonia/Controls/Tool.cs @@ -19,39 +19,13 @@ namespace Dock.Model.Avalonia.Controls; [DataContract(IsReference = true)] public class Tool : DockableBase, ITool, IDocument, IToolContent, ITemplate, IRecyclingDataTemplate { - private double _minWidth = double.NaN; - private double _maxWidth = double.NaN; - private double _minHeight = double.NaN; - private double _maxHeight = double.NaN; /// /// Defines the property. /// public static readonly StyledProperty ContentProperty = AvaloniaProperty.Register(nameof(Content)); - /// - /// Defines the property. - /// - public static readonly DirectProperty MinWidthProperty = - AvaloniaProperty.RegisterDirect(nameof(MinWidth), o => o.MinWidth, (o, v) => o.MinWidth = v, double.NaN); - /// - /// Defines the property. - /// - public static readonly DirectProperty MaxWidthProperty = - AvaloniaProperty.RegisterDirect(nameof(MaxWidth), o => o.MaxWidth, (o, v) => o.MaxWidth = v, double.NaN); - - /// - /// Defines the property. - /// - public static readonly DirectProperty MinHeightProperty = - AvaloniaProperty.RegisterDirect(nameof(MinHeight), o => o.MinHeight, (o, v) => o.MinHeight = v, double.NaN); - - /// - /// Defines the property. - /// - public static readonly DirectProperty MaxHeightProperty = - AvaloniaProperty.RegisterDirect(nameof(MaxHeight), o => o.MaxHeight, (o, v) => o.MaxHeight = v, double.NaN); /// /// Initializes new instance of the class. @@ -129,35 +103,4 @@ public bool Match(object? data) return TemplateHelper.Build(Content, this); } - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MinWidth - { - get => _minWidth; - set => SetAndRaise(MinWidthProperty, ref _minWidth, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MaxWidth - { - get => _maxWidth; - set => SetAndRaise(MaxWidthProperty, ref _maxWidth, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MinHeight - { - get => _minHeight; - set => SetAndRaise(MinHeightProperty, ref _minHeight, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MaxHeight - { - get => _maxHeight; - set => SetAndRaise(MaxHeightProperty, ref _maxHeight, value); - } } diff --git a/src/Dock.Model.Avalonia/Core/DockableBase.cs b/src/Dock.Model.Avalonia/Core/DockableBase.cs index 76a16e156..2a2a66cd2 100644 --- a/src/Dock.Model.Avalonia/Core/DockableBase.cs +++ b/src/Dock.Model.Avalonia/Core/DockableBase.cs @@ -110,6 +110,30 @@ public abstract class DockableBase : ReactiveBase, IDockable public static readonly DirectProperty CanDropProperty = AvaloniaProperty.RegisterDirect(nameof(CanDrop), o => o.CanDrop, (o, v) => o.CanDrop = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty MinWidthProperty = + AvaloniaProperty.RegisterDirect(nameof(MinWidth), o => o.MinWidth, (o, v) => o.MinWidth = v, double.NaN); + + /// + /// Defines the property. + /// + public static readonly DirectProperty MaxWidthProperty = + AvaloniaProperty.RegisterDirect(nameof(MaxWidth), o => o.MaxWidth, (o, v) => o.MaxWidth = v, double.NaN); + + /// + /// Defines the property. + /// + public static readonly DirectProperty MinHeightProperty = + AvaloniaProperty.RegisterDirect(nameof(MinHeight), o => o.MinHeight, (o, v) => o.MinHeight = v, double.NaN); + + /// + /// Defines the property. + /// + public static readonly DirectProperty MaxHeightProperty = + AvaloniaProperty.RegisterDirect(nameof(MaxHeight), o => o.MaxHeight, (o, v) => o.MaxHeight = v, double.NaN); + private readonly TrackingAdapter _trackingAdapter; private string _id = string.Empty; private string _title = string.Empty; @@ -125,6 +149,10 @@ public abstract class DockableBase : ReactiveBase, IDockable private bool _canFloat = true; private bool _canDrag = true; private bool _canDrop = true; + private double _minWidth = double.NaN; + private double _maxWidth = double.NaN; + private double _minHeight = double.NaN; + private double _maxHeight = double.NaN; /// /// Initializes new instance of the class. @@ -217,6 +245,42 @@ public double Proportion set => SetAndRaise(ProportionProperty, ref _proportion, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + [JsonPropertyName("MinWidth")] + public double MinWidth + { + get => _minWidth; + set => SetAndRaise(MinWidthProperty, ref _minWidth, value); + } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + [JsonPropertyName("MaxWidth")] + public double MaxWidth + { + get => _maxWidth; + set => SetAndRaise(MaxWidthProperty, ref _maxWidth, value); + } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + [JsonPropertyName("MinHeight")] + public double MinHeight + { + get => _minHeight; + set => SetAndRaise(MinHeightProperty, ref _minHeight, value); + } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + [JsonPropertyName("MaxHeight")] + public double MaxHeight + { + get => _maxHeight; + set => SetAndRaise(MaxHeightProperty, ref _maxHeight, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] [JsonPropertyName("CanClose")] diff --git a/src/Dock.Model.Mvvm/Controls/Tool.cs b/src/Dock.Model.Mvvm/Controls/Tool.cs index 5db82fb17..4f9dd618b 100644 --- a/src/Dock.Model.Mvvm/Controls/Tool.cs +++ b/src/Dock.Model.Mvvm/Controls/Tool.cs @@ -12,40 +12,4 @@ namespace Dock.Model.Mvvm.Controls; [DataContract(IsReference = true)] public class Tool : DockableBase, ITool, IDocument { - private double _minWidth = double.NaN; - private double _maxWidth = double.NaN; - private double _minHeight = double.NaN; - private double _maxHeight = double.NaN; - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MinWidth - { - get => _minWidth; - set => SetProperty(ref _minWidth, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MaxWidth - { - get => _maxWidth; - set => SetProperty(ref _maxWidth, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MinHeight - { - get => _minHeight; - set => SetProperty(ref _minHeight, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MaxHeight - { - get => _maxHeight; - set => SetProperty(ref _maxHeight, value); - } } diff --git a/src/Dock.Model.Mvvm/Core/DockableBase.cs b/src/Dock.Model.Mvvm/Core/DockableBase.cs index 916197dbf..595bee6de 100644 --- a/src/Dock.Model.Mvvm/Core/DockableBase.cs +++ b/src/Dock.Model.Mvvm/Core/DockableBase.cs @@ -27,6 +27,10 @@ public abstract class DockableBase : ReactiveBase, IDockable private bool _canFloat = true; private bool _canDrag = true; private bool _canDrop = true; + private double _minWidth = double.NaN; + private double _maxWidth = double.NaN; + private double _minHeight = double.NaN; + private double _maxHeight = double.NaN; /// /// Initializes new instance of the class. @@ -108,6 +112,38 @@ public double Proportion set => SetProperty(ref _proportion, value); } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public double MinWidth + { + get => _minWidth; + set => SetProperty(ref _minWidth, value); + } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public double MaxWidth + { + get => _maxWidth; + set => SetProperty(ref _maxWidth, value); + } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public double MinHeight + { + get => _minHeight; + set => SetProperty(ref _minHeight, value); + } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public double MaxHeight + { + get => _maxHeight; + set => SetProperty(ref _maxHeight, value); + } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public bool CanClose diff --git a/src/Dock.Model.ReactiveUI/Controls/Tool.cs b/src/Dock.Model.ReactiveUI/Controls/Tool.cs index 93809d032..e513d89f5 100644 --- a/src/Dock.Model.ReactiveUI/Controls/Tool.cs +++ b/src/Dock.Model.ReactiveUI/Controls/Tool.cs @@ -13,40 +13,4 @@ namespace Dock.Model.ReactiveUI.Controls; [DataContract(IsReference = true)] public partial class Tool : DockableBase, ITool, IDocument { - private double _minWidth = double.NaN; - private double _maxWidth = double.NaN; - private double _minHeight = double.NaN; - private double _maxHeight = double.NaN; - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MinWidth - { - get => _minWidth; - set => this.RaiseAndSetIfChanged(ref _minWidth, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MaxWidth - { - get => _maxWidth; - set => this.RaiseAndSetIfChanged(ref _maxWidth, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MinHeight - { - get => _minHeight; - set => this.RaiseAndSetIfChanged(ref _minHeight, value); - } - - /// - [DataMember(IsRequired = false, EmitDefaultValue = true)] - public double MaxHeight - { - get => _maxHeight; - set => this.RaiseAndSetIfChanged(ref _maxHeight, value); - } } diff --git a/src/Dock.Model.ReactiveUI/Core/DockableBase.cs b/src/Dock.Model.ReactiveUI/Core/DockableBase.cs index 360878c82..d41dda369 100644 --- a/src/Dock.Model.ReactiveUI/Core/DockableBase.cs +++ b/src/Dock.Model.ReactiveUI/Core/DockableBase.cs @@ -28,6 +28,10 @@ protected DockableBase() _canFloat = true; _canDrag = true; _canDrop = true; + _minWidth = double.NaN; + _maxWidth = double.NaN; + _minHeight = double.NaN; + _maxHeight = double.NaN; _trackingAdapter = new TrackingAdapter(); } @@ -67,6 +71,22 @@ protected DockableBase() [DataMember(IsRequired = false, EmitDefaultValue = true)] public partial double Proportion { get; set; } + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public partial double MinWidth { get; set; } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public partial double MaxWidth { get; set; } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public partial double MinHeight { get; set; } + + /// + [DataMember(IsRequired = false, EmitDefaultValue = true)] + public partial double MaxHeight { get; set; } + /// [DataMember(IsRequired = false, EmitDefaultValue = true)] public partial bool CanClose { get; set; } diff --git a/src/Dock.Model/Controls/ITool.cs b/src/Dock.Model/Controls/ITool.cs index dd3bcb16c..b5759ad64 100644 --- a/src/Dock.Model/Controls/ITool.cs +++ b/src/Dock.Model/Controls/ITool.cs @@ -9,23 +9,4 @@ namespace Dock.Model.Controls; /// public interface ITool : IDockable { - /// - /// Gets or sets minimum width. - /// - double MinWidth { get; set; } - - /// - /// Gets or sets maximum width. - /// - double MaxWidth { get; set; } - - /// - /// Gets or sets minimum height. - /// - double MinHeight { get; set; } - - /// - /// Gets or sets maximum height. - /// - double MaxHeight { get; set; } } diff --git a/src/Dock.Model/Core/IDockable.cs b/src/Dock.Model/Core/IDockable.cs index 50f4bff95..9381c03a7 100644 --- a/src/Dock.Model/Core/IDockable.cs +++ b/src/Dock.Model/Core/IDockable.cs @@ -50,11 +50,31 @@ public interface IDockable : IControlRecyclingIdProvider /// bool IsCollapsable { get; set; } - /// - /// Gets or sets splitter proportion. - /// + /// + /// Gets or sets splitter proportion. + /// double Proportion { get; set; } + /// + /// Gets or sets minimum width. + /// + double MinWidth { get; set; } + + /// + /// Gets or sets maximum width. + /// + double MaxWidth { get; set; } + + /// + /// Gets or sets minimum height. + /// + double MinHeight { get; set; } + + /// + /// Gets or sets maximum height. + /// + double MaxHeight { get; set; } + /// /// Gets or sets if the dockable can be closed. /// From e39d9d927bd933bca523c1359072d077057e2833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 2 Jul 2025 11:42:53 +0200 Subject: [PATCH 2/5] Fix fixed-size detection in proportional panel --- .../ProportionalStackPanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs b/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs index e44b2f881..7f1dbcdf2 100644 --- a/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs +++ b/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs @@ -104,16 +104,16 @@ private void AssignProportions(Size size, double splitterThickness) { if (orientation == Orientation.Horizontal) { - if (!double.IsNaN(control.MinWidth)) + if (!double.IsNaN(control.MinWidth) && control.MinWidth > 0) return control.MinWidth; - if (!double.IsNaN(control.MaxWidth)) + if (!double.IsNaN(control.MaxWidth) && !double.IsPositiveInfinity(control.MaxWidth)) return control.MaxWidth; } else { - if (!double.IsNaN(control.MinHeight)) + if (!double.IsNaN(control.MinHeight) && control.MinHeight > 0) return control.MinHeight; - if (!double.IsNaN(control.MaxHeight)) + if (!double.IsNaN(control.MaxHeight) && !double.IsPositiveInfinity(control.MaxHeight)) return control.MaxHeight; } From 3082ab78250faefc5f0afcde775f3add417d0d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 2 Jul 2025 12:48:00 +0200 Subject: [PATCH 3/5] Clamp proportions using min/max sizes --- .../ProportionalStackPanel.cs | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs b/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs index 7f1dbcdf2..9ec799828 100644 --- a/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs +++ b/src/Dock.Controls.ProportionalStackPanel/ProportionalStackPanel.cs @@ -100,81 +100,91 @@ private void AssignProportions(Size size, double splitterThickness) } } - private static double? GetFixedSize(Control control, Orientation orientation) + private static double ClampProportion( + Control control, + Orientation orientation, + double available, + double proportion) { - if (orientation == Orientation.Horizontal) - { - if (!double.IsNaN(control.MinWidth) && control.MinWidth > 0) - return control.MinWidth; - if (!double.IsNaN(control.MaxWidth) && !double.IsPositiveInfinity(control.MaxWidth)) - return control.MaxWidth; - } - else + double min = orientation == Orientation.Horizontal ? control.MinWidth : control.MinHeight; + double max = orientation == Orientation.Horizontal ? control.MaxWidth : control.MaxHeight; + + var minProp = !double.IsNaN(min) && min > 0 ? min / available : 0.0; + var maxProp = !double.IsNaN(max) && !double.IsPositiveInfinity(max) ? max / available : double.PositiveInfinity; + +#if NETSTANDARD2_0 + var clamped = Clamp(proportion, minProp, maxProp); +#else + var clamped = Math.Clamp(proportion, minProp, maxProp); +#endif + return clamped; + +#if NETSTANDARD2_0 + static double Clamp(double value, double min, double max) { - if (!double.IsNaN(control.MinHeight) && control.MinHeight > 0) - return control.MinHeight; - if (!double.IsNaN(control.MaxHeight) && !double.IsPositiveInfinity(control.MaxHeight)) - return control.MaxHeight; + if (value < min) return min; + if (value > max) return max; + return value; } - - return null; +#endif } - private static void AssignProportionsInternal(Avalonia.Controls.Controls children, Size size, double splitterThickness, Orientation orientation) + private static void AssignProportionsInternal( + Avalonia.Controls.Controls children, + Size size, + double splitterThickness, + Orientation orientation) { var dimension = orientation == Orientation.Horizontal ? size.Width : size.Height; var dimensionWithoutSplitters = Math.Max(1.0, dimension - splitterThickness); var assignedProportion = 0.0; - var dynamicChildren = children.OfType().Where(c => - { - var isCollapsed = GetIsCollapsed(c); - var isSplitter = ProportionalStackPanelSplitter.IsSplitter(c, out _); - return !isSplitter && !isCollapsed && GetFixedSize(c, orientation) == null; - }).ToList(); - double dynamicSum = 0.0; + var unassignedProportions = 0; foreach (var control in children.OfType()) { var isCollapsed = GetIsCollapsed(control); var isSplitter = ProportionalStackPanelSplitter.IsSplitter(control, out _); - if (isSplitter) - continue; - var proportion = GetProportion(control); - if (isCollapsed) + if (!isSplitter) { - proportion = 0.0; - } + var proportion = GetProportion(control); + + if (isCollapsed) + { + proportion = 0.0; + } - var fixedSize = GetFixedSize(control, orientation); - if (fixedSize.HasValue) - { - proportion = fixedSize.Value / dimensionWithoutSplitters; - SetProportion(control, proportion); - assignedProportion += proportion; - } - else - { if (double.IsNaN(proportion)) { - proportion = 1.0; + unassignedProportions++; + } + else + { + proportion = ClampProportion(control, orientation, dimensionWithoutSplitters, proportion); + SetProportion(control, proportion); + assignedProportion += proportion; } - dynamicSum += proportion; } } - if (dynamicChildren.Count > 0) + if (unassignedProportions > 0) { - var available = Math.Max(0.0, 1.0 - assignedProportion); - foreach (var control in dynamicChildren) + var toAssign = assignedProportion; + foreach (var control in children.Where(c => + { + var isCollapsed = GetIsCollapsed(c); + return !isCollapsed && double.IsNaN(GetProportion(c)); + })) { - var p = GetProportion(control); - if (double.IsNaN(p)) p = 1.0; - var newProp = dynamicSum > 0 ? (p / dynamicSum) * available : available / dynamicChildren.Count; - SetProportion(control, newProp); + if (!ProportionalStackPanelSplitter.IsSplitter(control, out _)) + { + var proportion = (1.0 - toAssign) / unassignedProportions; + proportion = ClampProportion(control, orientation, dimensionWithoutSplitters, proportion); + SetProportion(control, proportion); + assignedProportion += proportion; + } } - assignedProportion = 1.0; } if (assignedProportion < 1) From 161cbfc26b9c76a0b639dbe3f98383e41f6a32fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 2 Jul 2025 13:12:13 +0200 Subject: [PATCH 4/5] Add tests for proportional panel min/max support --- .../Controls/ProportionalStackPanelTests.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs b/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs index df0aa9a74..ce4378f38 100644 --- a/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs +++ b/tests/Dock.Avalonia.UnitTests/Controls/ProportionalStackPanelTests.cs @@ -252,4 +252,91 @@ public void Lays_Out_Children_ItemsControl() Assert.Equal(new Rect(0, 0, 0, 0), items2?[2].Bounds); Assert.Equal(new Rect(0, 0, 0, 0), items2?[3].Bounds); } + + [Fact] + public void Respects_MinWidth_When_Assigning_Proportion() + { + var target = new ProportionalStackPanel + { + Width = 500, + Height = 100, + Orientation = Orientation.Horizontal, + Children = + { + new Border + { + MinWidth = 150, + [ProportionalStackPanel.ProportionProperty] = 0.1 + }, + new ProportionalStackPanelSplitter(), + new Border + { + [ProportionalStackPanel.ProportionProperty] = 0.5 + } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.True(target.Children[0].Bounds.Width >= 150); + } + + [Fact] + public void Respects_MaxWidth_When_Assigning_Proportion() + { + var target = new ProportionalStackPanel + { + Width = 300, + Height = 100, + Orientation = Orientation.Horizontal, + Children = + { + new Border + { + MaxWidth = 100, + [ProportionalStackPanel.ProportionProperty] = 0.9 + }, + new ProportionalStackPanelSplitter(), + new Border + { + [ProportionalStackPanel.ProportionProperty] = 0.7 + } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.True(target.Children[0].Bounds.Width <= 100); + } + + [Fact] + public void Respects_MinHeight_In_Vertical_Mode() + { + var target = new ProportionalStackPanel + { + Width = 100, + Height = 500, + Orientation = Orientation.Vertical, + Children = + { + new Border + { + MinHeight = 200, + [ProportionalStackPanel.ProportionProperty] = 0.1 + }, + new ProportionalStackPanelSplitter(), + new Border + { + [ProportionalStackPanel.ProportionProperty] = 0.5 + } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.True(target.Children[0].Bounds.Height >= 200); + } } From b3e9f013cf7d2f00a8d57bc97d78e21d6dee325d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 2 Jul 2025 14:01:16 +0200 Subject: [PATCH 5/5] docs: document new size properties --- docs/dock-dockable-properties.md | 4 ++++ docs/dock-reference.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/dock-dockable-properties.md b/docs/dock-dockable-properties.md index 6bef4fc1d..71755fd64 100644 --- a/docs/dock-dockable-properties.md +++ b/docs/dock-dockable-properties.md @@ -15,6 +15,10 @@ Dockable items such as documents, tools and docks implement the `IDockable` inte | `IsEmpty` | Indicates a placeholder dockable with no content. | | `IsCollapsable` | When `false`, the dock will remain even if it contains no children. | | `Proportion` | Size ratio used by `ProportionalDock`. | +| `MinWidth` | Optional minimum width. Overrides the current proportion if larger. | +| `MaxWidth` | Optional maximum width. Overrides the proportion if smaller. | +| `MinHeight` | Optional minimum height. Overrides the current proportion if larger. | +| `MaxHeight` | Optional maximum height. Overrides the proportion if smaller. | | `CanClose` | Whether the user can close the dockable via UI commands. | | `CanPin` | Allows pinning and unpinning of tools. | | `CanFloat` | Controls if the item may be detached into a floating window. | diff --git a/docs/dock-reference.md b/docs/dock-reference.md index 755d3dab3..93d0cd98d 100644 --- a/docs/dock-reference.md +++ b/docs/dock-reference.md @@ -6,7 +6,7 @@ This reference summarizes the most commonly used classes in Dock. It is based on | Type | Description | | --- | --- | -| `IDockable` | Base interface for items that can be shown in a dock. Provides `Id`, `Title`, `Context` and lifecycle methods like `OnClose`. | +| `IDockable` | Base interface for items that can be shown in a dock. Provides `Id`, `Title`, `Context`, optional size limits and lifecycle methods like `OnClose`. | | `IDock` | Extends `IDockable` with collections of visible dockables and commands such as `GoBack`, `GoForward`, `Navigate` or `Close`. | | `IRootDock` | The top level container. In addition to the `IDock` members it exposes pinned dock collections and commands to manage windows. | | `IProportionalDock` | A dock that lays out its children horizontally or vertically using a `Proportion` value. |