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.
pull/403/head
Steven Kirk 11 years ago
parent
commit
3beea47bf7
  1. 513
      src/Perspex.Controls/DockPanel.cs
  2. 40
      src/Perspex.SceneGraph/Rect.cs
  3. 62
      tests/Perspex.Controls.UnitTests/DockPanelTests.cs
  4. 1
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

513
src/Perspex.Controls/DockPanel.cs

@ -1,445 +1,176 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using System; 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() static DockPanel()
{ {
AffectsArrange(DockProperty); AffectsArrange(DockProperty);
} }
// ReSharper disable once UnusedMember.Global /// <summary>
public static Dock GetDock(PerspexObject perspexObject) /// 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 /// <summary>
public static void SetDock(PerspexObject element, Dock dock) /// 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 public bool LastChildFill
{ {
get { return GetValue(LastChildFillProperty); } get { return GetValue(LastChildFillProperty); }
set { SetValue(LastChildFillProperty, value); } set { SetValue(LastChildFillProperty, value); }
} }
protected override Size MeasureOverride(Size availableSize) /// <inheritdoc/>
{ protected override Size MeasureOverride(Size constraint)
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)
{ {
var requiredHorizontalLength = 0D; double usedWidth = 0.0;
var requiredVerticalLength = 0D; 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); // Get the child's desired size
Size remainingSize = new Size(
var dock = control.GetValue(DockProperty); Math.Max(0.0, constraint.Width - usedWidth),
if (IsHorizontal(dock)) Math.Max(0.0, constraint.Height - usedHeight));
{ element.Measure(remainingSize);
requiredHorizontalLength += control.DesiredSize.Width; Size desiredSize = element.DesiredSize;
}
else // 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); maximumWidth = Math.Max(maximumWidth, usedWidth);
} maximumHeight = Math.Max(maximumHeight, usedHeight);
return new Size(maximumWidth, maximumHeight);
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);
} }
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); double left = 0.0;
var marginsCutout = margin.AsThickness(); double top = 0.0;
var withoutMargins = containerRect.Deflate(marginsCutout); double right = 0.0;
double bottom = 0.0;
layoutable.Arrange(withoutMargins);
}
private class DockingArranger // Arrange each of the Children
{ var children = Children;
public Margin UsedMargin { get; private set; } 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); // Determine the remaining space left to arrange the element
var rightArranger = new RightDocker(availableSize); Rect remainingRect = new Rect(
var topArranger = new LeftDocker(availableSize.Swap()); left,
var bottomArranger = new RightDocker(availableSize.Swap()); top,
Math.Max(0.0, arrangeSize.Width - left - right),
UsedMargin = new Margin(); Math.Max(0.0, arrangeSize.Height - top - bottom));
foreach (var control in children) // 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; Size desiredSize = element.DesiredSize;
var dock = control.GetValue(DockProperty); switch (GetDock(element))
switch (dock)
{ {
case Dock.Left: case Dock.Left:
dockedRect = leftArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments()); left += desiredSize.Width;
remainingRect = remainingRect.WithWidth(desiredSize.Width);
break; break;
case Dock.Top: case Dock.Top:
UsedMargin.Swap(); top += desiredSize.Height;
dockedRect = topArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap(); remainingRect = remainingRect.WithHeight(desiredSize.Height);
UsedMargin.Swap();
break; break;
case Dock.Right: 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; break;
case Dock.Bottom: case Dock.Bottom:
UsedMargin.Swap(); bottom += desiredSize.Height;
dockedRect = bottomArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap(); remainingRect = new Rect(
UsedMargin.Swap(); remainingRect.X,
Math.Max(0.0, arrangeSize.Height - bottom),
remainingRect.Width,
desiredSize.Height);
break; break;
default:
throw new InvalidOperationException($"Invalid dock value {dock}");
} }
control.Arrange(dockedRect);
} }
return availableSize; element.Arrange(remainingRect);
} index++;
}
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);
} }
protected double AccumulatedOffset { get; set; } return arrangeSize;
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);
} }
} }
} }

40
src/Perspex.SceneGraph/Rect.cs

@ -402,6 +402,46 @@ namespace Perspex
return new Rect(Position + offset, Size); 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> /// <summary>
/// Returns the string representation of the rectangle. /// Returns the string representation of the rectangle.
/// </summary> /// </summary>

62
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
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

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

Loading…
Cancel
Save