diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index 7d040bc2523..2bd90a980fa 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -37,12 +37,29 @@ public class DockPanel : Panel nameof(LastChildFill), defaultValue: true); + /// + /// Identifies the HorizontalSpacing dependency property. + /// + /// The identifier for the dependency property. + public static readonly StyledProperty HorizontalSpacingProperty = + AvaloniaProperty.Register( + nameof(HorizontalSpacing)); + + /// + /// Identifies the VerticalSpacing dependency property. + /// + /// The identifier for the dependency property. + public static readonly StyledProperty VerticalSpacingProperty = + AvaloniaProperty.Register( + nameof(VerticalSpacing)); + /// /// Initializes static members of the class. /// static DockPanel() { AffectsParentMeasure(DockProperty); + AffectsMeasure(LastChildFillProperty, HorizontalSpacingProperty, VerticalSpacingProperty); } /// @@ -75,39 +92,56 @@ public bool LastChildFill set => SetValue(LastChildFillProperty, value); } + /// + /// Gets or sets the horizontal distance between the child objects. + /// + public double HorizontalSpacing + { + get => GetValue(HorizontalSpacingProperty); + set => SetValue(HorizontalSpacingProperty, value); + } + + /// + /// Gets or sets the vertical distance between the child objects. + /// + public double VerticalSpacing + { + get => GetValue(VerticalSpacingProperty); + set => SetValue(VerticalSpacingProperty, value); + } + + /// /// Updates DesiredSize of the DockPanel. Called by parent Control. This is the first pass of layout. /// /// /// Children are measured based on their sizing properties and . - /// Each child is allowed to consume all of the space on the side on which it is docked; Left/Right docked + /// Each child is allowed to consume all the space on the side on which it is docked; Left/Right docked /// children are granted all vertical space for their entire width, and Top/Bottom docked children are /// granted all horizontal space for their entire height. /// - /// Constraint size is an "upper limit" that the return value should not exceed. + /// Constraint size is an "upper limit" that the return value should not exceed. /// The Panel's desired size. - protected override Size MeasureOverride(Size constraint) + protected override Size MeasureOverride(Size availableSize) { - var children = Children; + var parentWidth = 0d; + var parentHeight = 0d; + var accumulatedWidth = 0d; + var accumulatedHeight = 0d; - double parentWidth = 0; // Our current required width due to children thus far. - double parentHeight = 0; // Our current required height due to children thus far. - double accumulatedWidth = 0; // Total width consumed by children. - double accumulatedHeight = 0; // Total height consumed by children. + var horizontalSpacing = false; + var verticalSpacing = false; + var childrenCount = LastChildFill ? Children.Count - 1 : Children.Count; - for (int i = 0, count = children.Count; i < count; ++i) + for (var index = 0; index < childrenCount; ++index) { - var child = children[i]; - Size childConstraint; // Contains the suggested input constraint for this child. - Size childDesiredSize; // Contains the return size from child measure. - - // Child constraint is the remaining size; this is total size minus size consumed by previous children. - childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), - Math.Max(0.0, constraint.Height - accumulatedHeight)); + var child = Children[index]; + var childConstraint = new Size( + Math.Max(0, availableSize.Width - accumulatedWidth), + Math.Max(0, availableSize.Height - accumulatedHeight)); - // Measure child. child.Measure(childConstraint); - childDesiredSize = child.DesiredSize; + var childDesiredSize = child.DesiredSize; // Now, we adjust: // 1. Size consumed by children (accumulatedSize). This will be used when computing subsequent @@ -119,88 +153,125 @@ protected override Size MeasureOverride(Size constraint) // will deal with computing our minimum size (parentSize) due to that accumulation. // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does // not accumulate: Width for Top/Bottom, Height for Left/Right. - switch (GetDock(child)) + switch (child.GetValue(DockProperty)) { case Dock.Left: case Dock.Right: parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); + if (child.IsVisible) + { + accumulatedWidth += HorizontalSpacing; + horizontalSpacing = true; + } accumulatedWidth += childDesiredSize.Width; break; case Dock.Top: case Dock.Bottom: parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); + if (child.IsVisible) + { + accumulatedHeight += VerticalSpacing; + verticalSpacing = true; + } accumulatedHeight += childDesiredSize.Height; break; } } + if (LastChildFill) + { + var child = Children[Children.Count - 1]; + var childConstraint = new Size( + Math.Max(0, availableSize.Width - accumulatedWidth), + Math.Max(0, availableSize.Height - accumulatedHeight)); + + child.Measure(childConstraint); + var childDesiredSize = child.DesiredSize; + parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); + parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); + accumulatedHeight += childDesiredSize.Height; + accumulatedWidth += childDesiredSize.Width; + } + else + { + if (horizontalSpacing) + accumulatedWidth -= HorizontalSpacing; + if (verticalSpacing) + accumulatedHeight -= VerticalSpacing; + } + // Make sure the final accumulated size is reflected in parentSize. parentWidth = Math.Max(parentWidth, accumulatedWidth); parentHeight = Math.Max(parentHeight, accumulatedHeight); - - return (new Size(parentWidth, parentHeight)); + return new Size(parentWidth, parentHeight); } /// /// DockPanel computes a position and final size for each of its children based upon their /// enum and sizing properties. /// - /// Size that DockPanel will assume to position children. - protected override Size ArrangeOverride(Size arrangeSize) + /// Size that DockPanel will assume to position children. + protected override Size ArrangeOverride(Size finalSize) { - var children = Children; - int totalChildrenCount = children.Count; - int nonFillChildrenCount = totalChildrenCount - (LastChildFill ? 1 : 0); + if (Children.Count is 0) + return finalSize; - double accumulatedLeft = 0; - double accumulatedTop = 0; - double accumulatedRight = 0; - double accumulatedBottom = 0; + var currentBounds = new Rect(finalSize); + var childrenCount = LastChildFill ? Children.Count - 1 : Children.Count; - for (int i = 0; i < totalChildrenCount; ++i) + for (var index = 0; index < childrenCount; ++index) { - var child = children[i]; + var child = Children[index]; + if (!child.IsVisible) + continue; - Size childDesiredSize = child.DesiredSize; - Rect rcChild = new Rect( - accumulatedLeft, - accumulatedTop, - Math.Max(0.0, arrangeSize.Width - (accumulatedLeft + accumulatedRight)), - Math.Max(0.0, arrangeSize.Height - (accumulatedTop + accumulatedBottom))); - - if (i < nonFillChildrenCount) + var dock = child.GetValue(DockProperty); + double width, height; + switch (dock) { - switch (GetDock(child)) - { - case Dock.Left: - accumulatedLeft += childDesiredSize.Width; - rcChild = rcChild.WithWidth(childDesiredSize.Width); - break; - - case Dock.Right: - accumulatedRight += childDesiredSize.Width; - rcChild = rcChild.WithX(Math.Max(0.0, arrangeSize.Width - accumulatedRight)); - rcChild = rcChild.WithWidth(childDesiredSize.Width); - break; - - case Dock.Top: - accumulatedTop += childDesiredSize.Height; - rcChild = rcChild.WithHeight(childDesiredSize.Height); - break; - - case Dock.Bottom: - accumulatedBottom += childDesiredSize.Height; - rcChild = rcChild.WithY(Math.Max(0.0, arrangeSize.Height - accumulatedBottom)); - rcChild = rcChild.WithHeight(childDesiredSize.Height); - break; - } + case Dock.Left: + + width = Math.Min(child.DesiredSize.Width, currentBounds.Width); + child.Arrange(currentBounds.WithWidth(width)); + width += HorizontalSpacing; + currentBounds = new Rect(currentBounds.X + width, currentBounds.Y, Math.Max(0, currentBounds.Width - width), currentBounds.Height); + + break; + case Dock.Top: + + height = Math.Min(child.DesiredSize.Height, currentBounds.Height); + child.Arrange(currentBounds.WithHeight(height)); + height += VerticalSpacing; + currentBounds = new Rect(currentBounds.X, currentBounds.Y + height, currentBounds.Width, Math.Max(0, currentBounds.Height - height)); + + break; + case Dock.Right: + + width = Math.Min(child.DesiredSize.Width, currentBounds.Width); + child.Arrange(new Rect(currentBounds.X + currentBounds.Width - width, currentBounds.Y, width, currentBounds.Height)); + width += HorizontalSpacing; + currentBounds = currentBounds.WithWidth(Math.Max(0, currentBounds.Width - width)); + + break; + case Dock.Bottom: + + height = Math.Min(child.DesiredSize.Height, currentBounds.Height); + child.Arrange(new Rect(currentBounds.X, currentBounds.Y + currentBounds.Height - height, currentBounds.Width, height)); + height += VerticalSpacing; + currentBounds = currentBounds.WithHeight(Math.Max(0, currentBounds.Height - height)); + + break; } + } - child.Arrange(rcChild); + if (LastChildFill) + { + var child = Children[Children.Count - 1]; + child.Arrange(new Rect(currentBounds.X, currentBounds.Y, currentBounds.Width, currentBounds.Height)); } - return (arrangeSize); + return finalSize; } } } diff --git a/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs b/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs index 4da1ef5a100..d00ef70cde9 100644 --- a/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs @@ -56,6 +56,34 @@ public void Should_Dock_Controls_Vertical_First() Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds); } + [Fact] + public void Should_Dock_Controls_With_Spacing() + { + var target = new DockPanel + { + HorizontalSpacing = 10, + VerticalSpacing = 10, + Children = + { + new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top }, + new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom }, + new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left }, + new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right }, + new Border { }, + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 500, 520), target.Bounds); + Assert.Equal(new Rect(0, 0, 500, 50), target.Children[0].Bounds); + Assert.Equal(new Rect(0, 470, 500, 50), target.Children[1].Bounds); + Assert.Equal(new Rect(0, 60, 50, 400), target.Children[2].Bounds); + Assert.Equal(new Rect(450, 60, 50, 400), target.Children[3].Bounds); + Assert.Equal(new Rect(60, 60, 380, 400), target.Children[4].Bounds); + } + [Fact] public void Changing_Child_Dock_Invalidates_Measure() {