diff --git a/src/Perspex.Controls/DockPanel.cs b/src/Perspex.Controls/DockPanel.cs
index a1224a5272..8108a8c3cb 100644
--- a/src/Perspex.Controls/DockPanel.cs
+++ b/src/Perspex.Controls/DockPanel.cs
@@ -1,445 +1,176 @@
namespace Perspex.Controls
{
using System;
- using System.Collections.Generic;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using Layout;
- public class DockPanel : Panel
+ ///
+ /// Defines the available docking modes for a control in a .
+ ///
+ public enum Dock
{
- public static readonly PerspexProperty DockProperty = PerspexProperty.RegisterAttached("Dock");
+ Left = 0,
+ Bottom,
+ Right,
+ Top
+ }
+ ///
+ /// A panel which arranges its children at the top, bottom, left, right or center.
+ ///
+ public class DockPanel : Panel
+ {
+ ///
+ /// Defines the Dock attached property.
+ ///
+ public static readonly PerspexProperty DockProperty =
+ PerspexProperty.RegisterAttached("Dock");
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly PerspexProperty LastChildFillProperty =
+ PerspexProperty.Register(
+ nameof(LastChildFillProperty),
+ defaultValue: true);
+
+ ///
+ /// Initializes static members of the class.
+ ///
static DockPanel()
{
AffectsArrange(DockProperty);
}
- // ReSharper disable once UnusedMember.Global
- public static Dock GetDock(PerspexObject perspexObject)
+ ///
+ /// Gets the value of the Dock attached property on the specified control.
+ ///
+ /// The control.
+ /// The Dock attached property.
+ public static Dock GetDock(Control control)
{
- return perspexObject.GetValue(DockProperty);
+ return control.GetValue(DockProperty);
}
- // ReSharper disable once UnusedMember.Global
- public static void SetDock(PerspexObject element, Dock dock)
+ ///
+ /// Sets the value of the Dock attached property on the specified control.
+ ///
+ /// The control.
+ /// The value of the Dock property.
+ public static void SetDock(Control control, Dock value)
{
- element.SetValue(DockProperty, dock);
+ control.SetValue(DockProperty, value);
}
- public static readonly PerspexProperty LastChildFillProperty = PerspexProperty.Register(nameof(LastChildFillProperty), defaultValue: true);
-
+ ///
+ /// Gets or sets a value which indicates whether the last child of the
+ /// fills the remaining space in the panel.
+ ///
public bool LastChildFill
{
get { return GetValue(LastChildFillProperty); }
set { SetValue(LastChildFillProperty, value); }
}
- protected override Size MeasureOverride(Size availableSize)
- {
- if (!LastChildFill)
- {
- return MeasureItemsThatWillBeDocked(availableSize, Children);
- }
-
- var sizeRequiredByDockingItems = MeasureItemsThatWillBeDocked(availableSize, Children.WithoutLast());
- var elementThatWillFill = Children.Last();
- elementThatWillFill.Measure(availableSize - sizeRequiredByDockingItems);
- var finalSize = sizeRequiredByDockingItems.Inflate(new Thickness(elementThatWillFill.DesiredSize.Width, elementThatWillFill.DesiredSize.Height));
- return finalSize;
- }
-
- private static Size MeasureItemsThatWillBeDocked(Size availableSize, IEnumerable children)
+ ///
+ protected override Size MeasureOverride(Size constraint)
{
- var requiredHorizontalLength = 0D;
- var requiredVerticalLength = 0D;
+ double usedWidth = 0.0;
+ double usedHeight = 0.0;
+ double maximumWidth = 0.0;
+ double maximumHeight = 0.0;
- foreach (var control in children)
+ // Measure each of the Children
+ foreach (Control element in Children)
{
- control.Measure(availableSize);
-
- var dock = control.GetValue(DockProperty);
- if (IsHorizontal(dock))
- {
- requiredHorizontalLength += control.DesiredSize.Width;
- }
- else
+ // Get the child's desired size
+ Size remainingSize = new Size(
+ Math.Max(0.0, constraint.Width - usedWidth),
+ Math.Max(0.0, constraint.Height - usedHeight));
+ element.Measure(remainingSize);
+ Size desiredSize = element.DesiredSize;
+
+ // Decrease the remaining space for the rest of the children
+ switch (GetDock(element))
{
- requiredVerticalLength += control.DesiredSize.Height;
+ case Dock.Left:
+ case Dock.Right:
+ maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height);
+ usedWidth += desiredSize.Width;
+ break;
+ case Dock.Top:
+ case Dock.Bottom:
+ maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width);
+ usedHeight += desiredSize.Height;
+ break;
}
}
- return new Size(requiredHorizontalLength, requiredVerticalLength);
- }
-
- private static bool IsHorizontal(Dock dock)
- {
- return dock == Dock.Left || dock == Dock.Right;
- }
-
- protected override Size ArrangeOverride(Size finalSize)
- {
- if (!LastChildFill)
- {
- return ArrangeAllChildren(finalSize);
- }
- else
- {
- return ArrangeChildrenAndFillLastChild(finalSize);
- }
- }
-
- private Size ArrangeChildrenAndFillLastChild(Size finalSize)
- {
- var docker = new DockingArranger();
- var requiredSize = docker.ArrangeAndGetUsedSize(finalSize, Children.WithoutLast());
- ArrangeToFill(Children.Last(), finalSize, docker.UsedMargin);
- return requiredSize;
- }
-
- private Size ArrangeAllChildren(Size finalSize)
- {
- return new DockingArranger().ArrangeAndGetUsedSize(finalSize, Children);
+ maximumWidth = Math.Max(maximumWidth, usedWidth);
+ maximumHeight = Math.Max(maximumHeight, usedHeight);
+ return new Size(maximumWidth, maximumHeight);
}
- private static void ArrangeToFill(ILayoutable layoutable, Size containerSize, Margin margin)
+ ///
+ protected override Size ArrangeOverride(Size arrangeSize)
{
- var containerRect = new Rect(new Point(0, 0), containerSize);
- var marginsCutout = margin.AsThickness();
- var withoutMargins = containerRect.Deflate(marginsCutout);
-
- layoutable.Arrange(withoutMargins);
- }
+ double left = 0.0;
+ double top = 0.0;
+ double right = 0.0;
+ double bottom = 0.0;
- private class DockingArranger
- {
- public Margin UsedMargin { get; private set; }
+ // Arrange each of the Children
+ var children = Children;
+ int dockedCount = children.Count - (LastChildFill ? 1 : 0);
+ int index = 0;
- public Size ArrangeAndGetUsedSize(Size availableSize, IEnumerable children)
+ foreach (Control element in children)
{
- var leftArranger = new LeftDocker(availableSize);
- var rightArranger = new RightDocker(availableSize);
- var topArranger = new LeftDocker(availableSize.Swap());
- var bottomArranger = new RightDocker(availableSize.Swap());
-
- UsedMargin = new Margin();
-
- foreach (var control in children)
+ // Determine the remaining space left to arrange the element
+ Rect remainingRect = new Rect(
+ left,
+ top,
+ Math.Max(0.0, arrangeSize.Width - left - right),
+ Math.Max(0.0, arrangeSize.Height - top - bottom));
+
+ // Trim the remaining Rect to the docked size of the element
+ // (unless the element should fill the remaining space because
+ // of LastChildFill)
+ if (index < dockedCount)
{
- Rect dockedRect;
- var dock = control.GetValue(DockProperty);
- switch (dock)
+ Size desiredSize = element.DesiredSize;
+ switch (GetDock(element))
{
case Dock.Left:
- dockedRect = leftArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments());
+ left += desiredSize.Width;
+ remainingRect = remainingRect.WithWidth(desiredSize.Width);
break;
-
case Dock.Top:
- UsedMargin.Swap();
- dockedRect = topArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap();
- UsedMargin.Swap();
+ top += desiredSize.Height;
+ remainingRect = remainingRect.WithHeight(desiredSize.Height);
break;
-
case Dock.Right:
- dockedRect = rightArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments());
+ right += desiredSize.Width;
+ remainingRect = new Rect(
+ Math.Max(0.0, arrangeSize.Width - right),
+ remainingRect.Y,
+ desiredSize.Width,
+ remainingRect.Height);
break;
-
case Dock.Bottom:
- UsedMargin.Swap();
- dockedRect = bottomArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap();
- UsedMargin.Swap();
+ bottom += desiredSize.Height;
+ remainingRect = new Rect(
+ remainingRect.X,
+ Math.Max(0.0, arrangeSize.Height - bottom),
+ remainingRect.Width,
+ desiredSize.Height);
break;
-
- default:
- throw new InvalidOperationException($"Invalid dock value {dock}");
}
-
- control.Arrange(dockedRect);
}
- return availableSize;
- }
- }
-
- private class LeftDocker : Docker
- {
- public LeftDocker(Size availableSize) : base(availableSize)
- {
- }
-
- public override Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments)
- {
- var marginsCutout = margin.AsThickness();
- var availableRect = OriginalRect.Deflate(marginsCutout);
- var alignedRect = AlignToLeft(availableRect, childSize, alignments.Vertical);
-
- AccumulatedOffset += childSize.Width;
- margin.Horizontal = margin.Horizontal.Offset(childSize.Width, 0);
-
- return alignedRect;
- }
-
- private static Rect AlignToLeft(Rect availableRect, Size childSize, Alignment verticalAlignment)
- {
- return availableRect.AlignChild(childSize, Alignment.Start, verticalAlignment);
- }
- }
-
- private class RightDocker : Docker
- {
- public RightDocker(Size availableSize) : base(availableSize)
- {
- }
-
- public override Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments)
- {
- var marginsCutout = margin.AsThickness();
- var withoutMargins = OriginalRect.Deflate(marginsCutout);
- var finalRect = withoutMargins.AlignChild(childSize, Alignment.End, alignments.Vertical);
-
- AccumulatedOffset += childSize.Width;
- margin.Horizontal = margin.Horizontal.Offset(0, childSize.Width);
-
- return finalRect;
- }
- }
-
- private abstract class Docker
- {
- protected Docker(Size availableSize)
- {
- OriginalRect = new Rect(new Point(0, 0), availableSize);
+ element.Arrange(remainingRect);
+ index++;
}
- protected double AccumulatedOffset { get; set; }
-
- protected Rect OriginalRect { get; }
-
- public abstract Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments);
- }
- }
-
- public class Margin
- {
- public Segment Horizontal { get; set; }
- public Segment Vertical { get; set; }
- }
-
- public enum Alignment
- {
- Stretch, Start, Middle, End,
- }
-
- public static class SegmentMixin
- {
- public static Segment AlignToStart(this Segment container, double length)
- {
- return new Segment(container.Start, container.Start + length);
- }
-
- public static Segment AlignToEnd(this Segment container, double length)
- {
- return new Segment(container.End - length, container.End);
- }
-
- public static Segment AlignToMiddle(this Segment container, double length)
- {
- var start = container.Start + (container.Length - length) / 2;
- return new Segment(start, start + length);
- }
- }
-
- public struct Alignments
- {
- public Alignments(Alignment horizontal, Alignment vertical)
- {
- Horizontal = horizontal;
- Vertical = vertical;
- }
-
- public Alignment Horizontal { get; }
-
- public Alignment Vertical { get; }
- }
-
- public static class CoordinateMixin
- {
- private static Point Swap(this Point p)
- {
- return new Point(p.Y, p.X);
- }
-
- public static Size Swap(this Size s)
- {
- return new Size(s.Height, s.Width);
- }
-
- public static Rect Swap(this Rect r)
- {
- return new Rect(r.Position.Swap(), r.Size.Swap());
- }
-
- public static Segment Offset(this Segment l, double startOffset, double endOffset)
- {
- return new Segment(l.Start + startOffset, l.End + endOffset);
- }
-
- public static void Swap(this Margin m)
- {
- var v = m.Vertical;
- m.Vertical = m.Horizontal;
- m.Horizontal = v;
- }
-
- public static Thickness AsThickness(this Margin margin)
- {
- return new Thickness(margin.Horizontal.Start, margin.Vertical.Start, margin.Horizontal.End, margin.Vertical.End);
- }
-
- private static Alignment AsAlignment(this HorizontalAlignment horz)
- {
- switch (horz)
- {
- case HorizontalAlignment.Stretch:
- return Alignment.Stretch;
- case HorizontalAlignment.Left:
- return Alignment.Start;
- case HorizontalAlignment.Center:
- return Alignment.Middle;
- case HorizontalAlignment.Right:
- return Alignment.End;
- default:
- throw new ArgumentOutOfRangeException(nameof(horz), horz, null);
- }
- }
-
- private static Alignment AsAlignment(this VerticalAlignment vert)
- {
- switch (vert)
- {
- case VerticalAlignment.Stretch:
- return Alignment.Stretch;
- case VerticalAlignment.Top:
- return Alignment.Start;
- case VerticalAlignment.Center:
- return Alignment.Middle;
- case VerticalAlignment.Bottom:
- return Alignment.End;
- default:
- throw new ArgumentOutOfRangeException(nameof(vert), vert, null);
- }
- }
-
- public static Alignments GetAlignments(this ILayoutable layoutable)
- {
- return new Alignments(layoutable.HorizontalAlignment.AsAlignment(), layoutable.VerticalAlignment.AsAlignment());
- }
-
- public static Alignments Swap(this Alignments alignments)
- {
- return new Alignments(alignments.Vertical, alignments.Horizontal);
- }
- }
-
- public enum Dock
- {
- Left = 0,
- Bottom,
- Right,
- Top
- }
-
- public static class RectMixin
- {
- public static Rect AlignChild(this Rect container, Size childSize, Alignment horizontalAlignment, Alignment verticalAlignment)
- {
- var horzSegment = container.GetHorizontalCoordinates();
- var vertSegment = container.GetVerticalCoordinates();
-
- var horzResult = GetAlignedSegment(childSize.Width, horizontalAlignment, horzSegment);
- var vertResult = GetAlignedSegment(childSize.Height, verticalAlignment, vertSegment);
-
- return FromSegments(horzResult, vertResult);
- }
-
- private static Rect FromSegments(Segment horzSegment, Segment vertSegment)
- {
- return new Rect(horzSegment.Start, vertSegment.Start, horzSegment.Length, vertSegment.Length);
- }
-
- private static Segment GetAlignedSegment(double width, Alignment alignment, Segment horzSegment)
- {
- switch (alignment)
- {
- case Alignment.Start:
- return horzSegment.AlignToStart(width);
-
- case Alignment.Middle:
- return horzSegment.AlignToMiddle(width);
-
- case Alignment.End:
- return horzSegment.AlignToEnd(width);
-
- default:
- return new Segment(horzSegment.Start, horzSegment.End);
- }
- }
-
- private static Segment GetHorizontalCoordinates(this Rect rect)
- {
- return new Segment(rect.X, rect.Right);
- }
-
- private static Segment GetVerticalCoordinates(this Rect rect)
- {
- return new Segment(rect.Y, rect.Bottom);
- }
- }
-
- public struct Segment
- {
- public Segment(double start, double end)
- {
- Start = start;
- End = end;
- }
-
- public double Start { get; }
- public double End { get; }
-
- public double Length => End - Start;
-
- public override string ToString()
- {
- return $"Start: {Start}, End: {End}";
- }
- }
-
- public static class EnumerableMixin
- {
- private static IEnumerable Shrink(this IEnumerable source, int left, int right)
- {
- int i = 0;
- var buffer = new Queue(right + 1);
-
- foreach (T x in source)
- {
- if (i >= left) // Read past left many elements at the start
- {
- buffer.Enqueue(x);
- if (buffer.Count > right) // Build a buffer to drop right many elements at the end
- yield return buffer.Dequeue();
- }
- else i++;
- }
- }
- public static IEnumerable WithoutLast(this IEnumerable source, int n = 1)
- {
- return source.Shrink(0, n);
- }
- public static IEnumerable WithoutFirst(this IEnumerable source, int n = 1)
- {
- return source.Shrink(n, 0);
+ return arrangeSize;
}
}
}
\ No newline at end of file
diff --git a/src/Perspex.SceneGraph/Rect.cs b/src/Perspex.SceneGraph/Rect.cs
index af8a32a593..399623b579 100644
--- a/src/Perspex.SceneGraph/Rect.cs
+++ b/src/Perspex.SceneGraph/Rect.cs
@@ -402,6 +402,46 @@ namespace Perspex
return new Rect(Position + offset, Size);
}
+ ///
+ /// Returns a new with the specified X position.
+ ///
+ /// The x position.
+ /// The new .
+ public Rect WithX(double x)
+ {
+ return new Rect(x, _y, _width, _height);
+ }
+
+ ///
+ /// Returns a new with the specified Y position.
+ ///
+ /// The y position.
+ /// The new .
+ public Rect WithY(double y)
+ {
+ return new Rect(_x, y, _width, _height);
+ }
+
+ ///
+ /// Returns a new with the specified width.
+ ///
+ /// The width.
+ /// The new .
+ public Rect WithWidth(double width)
+ {
+ return new Rect(_x, _y, width, _height);
+ }
+
+ ///
+ /// Returns a new with the specified height.
+ ///
+ /// The height.
+ /// The new .
+ public Rect WithHeight(double height)
+ {
+ return new Rect(_x, _y, _width, height);
+ }
+
///
/// Returns the string representation of the rectangle.
///
diff --git a/tests/Perspex.Controls.UnitTests/DockPanelTests.cs b/tests/Perspex.Controls.UnitTests/DockPanelTests.cs
new file mode 100644
index 0000000000..7318e857d6
--- /dev/null
+++ b/tests/Perspex.Controls.UnitTests/DockPanelTests.cs
@@ -0,0 +1,62 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Xunit;
+
+namespace Perspex.Controls.UnitTests
+{
+ public class DockPanelTests
+ {
+ [Fact]
+ public void Should_Dock_Controls_Horizontal_First()
+ {
+ var target = new DockPanel
+ {
+ Children = new Controls
+ {
+ 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, 500), target.Bounds);
+ Assert.Equal(new Rect(0, 0, 500, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 450, 500, 50), target.Children[1].Bounds);
+ Assert.Equal(new Rect(0, 50, 50, 400), target.Children[2].Bounds);
+ Assert.Equal(new Rect(450, 50, 50, 400), target.Children[3].Bounds);
+ Assert.Equal(new Rect(50, 50, 400, 400), target.Children[4].Bounds);
+ }
+
+ [Fact]
+ public void Should_Dock_Controls_Vertical_First()
+ {
+ var target = new DockPanel
+ {
+ Children = new Controls
+ {
+ new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left },
+ new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right },
+ new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top },
+ new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom },
+ new Border { },
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Rect(0, 0, 600, 400), target.Bounds);
+ Assert.Equal(new Rect(0, 0, 50, 400), target.Children[0].Bounds);
+ Assert.Equal(new Rect(550, 0, 50, 400), target.Children[1].Bounds);
+ Assert.Equal(new Rect(50, 0, 500, 50), target.Children[2].Bounds);
+ Assert.Equal(new Rect(50, 350, 500, 50), target.Children[3].Bounds);
+ Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds);
+ }
+ }
+}
diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
index c4283ef06e..87230b7a55 100644
--- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
+++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
@@ -82,6 +82,7 @@
+