From 3beea47bf7df74a7d8879c568856f1e6bb4bc697 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Jan 2016 00:17:02 +0100 Subject: [PATCH] Replace DockPanel implementation. The old one was broken - replaced by the implementation from WinRTXamlToolkit which is both simpler and works correctly. Also added some tests. This closes #397, closes #348, closes #74. --- src/Perspex.Controls/DockPanel.cs | 513 +++++------------- src/Perspex.SceneGraph/Rect.cs | 40 ++ .../DockPanelTests.cs | 62 +++ .../Perspex.Controls.UnitTests.csproj | 1 + 4 files changed, 225 insertions(+), 391 deletions(-) create mode 100644 tests/Perspex.Controls.UnitTests/DockPanelTests.cs 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 @@ +