Browse Source

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.
Steven Kirk 9 years ago
parent
commit
3beea47bf7

+ 122 - 391
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
+    /// <summary>
+    /// Defines the available docking modes for a control in a <see cref="DockPanel"/>.
+    /// </summary>
+    public enum Dock
     {
-        public static readonly PerspexProperty<Dock> DockProperty = PerspexProperty.RegisterAttached<DockPanel, Control, Dock>("Dock");
+        Left = 0,
+        Bottom,
+        Right,
+        Top
+    }
 
+    /// <summary>
+    /// A panel which arranges its children at the top, bottom, left, right or center.
+    /// </summary>
+    public class DockPanel : Panel
+    {
+        /// <summary>
+        /// Defines the Dock attached property.
+        /// </summary>
+        public static readonly PerspexProperty<Dock> DockProperty =
+            PerspexProperty.RegisterAttached<DockPanel, Control, Dock>("Dock");
+
+        /// <summary>
+        /// Defines the <see cref="LastChildFill"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<bool> LastChildFillProperty =
+            PerspexProperty.Register<DockPanel, bool>(
+                nameof(LastChildFillProperty),
+                defaultValue: true);
+
+        /// <summary>
+        /// Initializes static members of the <see cref="DockPanel"/> class.
+        /// </summary>
         static DockPanel()
         {
             AffectsArrange(DockProperty);
         }
 
-        // ReSharper disable once UnusedMember.Global
-        public static Dock GetDock(PerspexObject perspexObject)
+        /// <summary>
+        /// Gets the value of the Dock attached property on the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <returns>The Dock attached property.</returns>
+        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)
+        /// <summary>
+        /// Sets the value of the Dock attached property on the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="value">The value of the Dock property.</param>
+        public static void SetDock(Control control, Dock value)
         {
-            element.SetValue(DockProperty, dock);
+            control.SetValue(DockProperty, value);
         }
 
-        public static readonly PerspexProperty<bool> LastChildFillProperty = PerspexProperty.Register<DockPanel, bool>(nameof(LastChildFillProperty), defaultValue: true);
-
+        /// <summary>
+        /// Gets or sets a value which indicates whether the last child of the 
+        /// <see cref="DockPanel"/> fills the remaining space in the panel.
+        /// </summary>
         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<IControl> children)
+        /// <inheritdoc/>
+        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)
+        /// <inheritdoc/>
+        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<IControl> 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<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
-        {
-            int i = 0;
-            var buffer = new Queue<T>(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<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
-        {
-            return source.Shrink(0, n);
-        }
-        public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
-        {
-            return source.Shrink(n, 0);
+            return arrangeSize;
         }
     }
 }

+ 40 - 0
src/Perspex.SceneGraph/Rect.cs

@@ -402,6 +402,46 @@ namespace Perspex
             return new Rect(Position + offset, Size);
         }
 
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified X position.
+        /// </summary>
+        /// <param name="x">The x position.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithX(double x)
+        {
+            return new Rect(x, _y, _width, _height);
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified Y position.
+        /// </summary>
+        /// <param name="y">The y position.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithY(double y)
+        {
+            return new Rect(_x, y, _width, _height);
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified width.
+        /// </summary>
+        /// <param name="width">The width.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithWidth(double width)
+        {
+            return new Rect(_x, _y, width, _height);
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified height.
+        /// </summary>
+        /// <param name="height">The height.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithHeight(double height)
+        {
+            return new Rect(_x, _y, _width, height);
+        }
+
         /// <summary>
         /// Returns the string representation of the rectangle.
         /// </summary>

+ 62 - 0
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);
+        }
+    }
+}

+ 1 - 0
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@@ -82,6 +82,7 @@
   </Choose>
   <ItemGroup>
     <Compile Include="ClassesTests.cs" />
+    <Compile Include="DockPanelTests.cs" />
     <Compile Include="EnumerableExtensions.cs" />
     <Compile Include="GridSplitterTests.cs" />
     <Compile Include="GridTests.cs" />