4 changed files with 347 additions and 0 deletions
@ -0,0 +1,249 @@ |
|||||
|
// 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 System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Linq; |
||||
|
|
||||
|
using Perspex.Input; |
||||
|
|
||||
|
using static System.Math; |
||||
|
|
||||
|
namespace Perspex.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Positions child elements in sequential position from left to right,
|
||||
|
/// breaking content to the next line at the edge of the containing box.
|
||||
|
/// Subsequent ordering happens sequentially from top to bottom or from right to left,
|
||||
|
/// depending on the value of the Orientation property.
|
||||
|
/// </summary>
|
||||
|
internal class WrapPanel : Panel, INavigableContainer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Orientation"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly PerspexProperty<Orientation> OrientationProperty = |
||||
|
PerspexProperty.Register<WrapPanel, Orientation>(nameof(Orientation), defaultValue: Orientation.Horizontal); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes static members of the <see cref="WrapPanel"/> class.
|
||||
|
/// </summary>
|
||||
|
static WrapPanel() |
||||
|
{ |
||||
|
AffectsMeasure(OrientationProperty); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the orientation in which child controls will be layed out.
|
||||
|
/// </summary>
|
||||
|
public Orientation Orientation { get { return GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the next control in the specified direction.
|
||||
|
/// </summary>
|
||||
|
/// <param name="direction">The movement direction.</param>
|
||||
|
/// <param name="from">The control from which movement begins.</param>
|
||||
|
/// <returns>The control.</returns>
|
||||
|
IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) |
||||
|
{ |
||||
|
var horiz = Orientation == Orientation.Horizontal; |
||||
|
int index = Children.IndexOf((IControl)from); |
||||
|
|
||||
|
switch (direction) |
||||
|
{ |
||||
|
case FocusNavigationDirection.First: |
||||
|
index = 0; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Last: |
||||
|
index = Children.Count - 1; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Next: |
||||
|
++index; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Previous: |
||||
|
--index; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Left: |
||||
|
index = horiz ? index - 1 : -1; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Right: |
||||
|
index = horiz ? index + 1 : -1; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Up: |
||||
|
index = horiz ? -1 : index - 1; |
||||
|
break; |
||||
|
case FocusNavigationDirection.Down: |
||||
|
index = horiz ? -1 : index + 1; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (index >= 0 && index < Children.Count) |
||||
|
{ |
||||
|
return Children[index]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size); |
||||
|
|
||||
|
private UVSize CreateUVSize() => new UVSize(Orientation); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Size MeasureOverride(Size availableSize) |
||||
|
{ |
||||
|
var desiredSize = CreateUVSize(); |
||||
|
var lineSize = CreateUVSize(); |
||||
|
var uvAvailableSize = CreateUVSize(availableSize); |
||||
|
|
||||
|
foreach (var child in Children) |
||||
|
{ |
||||
|
child.Measure(availableSize); |
||||
|
var childSize = CreateUVSize(child.DesiredSize); |
||||
|
if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line
|
||||
|
{ |
||||
|
lineSize.U += childSize.U; |
||||
|
lineSize.V = Max(lineSize.V, childSize.V); |
||||
|
} |
||||
|
else // moving to next line
|
||||
|
{ |
||||
|
desiredSize.U = Max(lineSize.U, uvAvailableSize.U); |
||||
|
desiredSize.V += lineSize.V; |
||||
|
lineSize = childSize; |
||||
|
} |
||||
|
} |
||||
|
// last element
|
||||
|
desiredSize.U = Max(lineSize.U, uvAvailableSize.U); |
||||
|
desiredSize.V += lineSize.V; |
||||
|
|
||||
|
return desiredSize.ToSize(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Size ArrangeOverride(Size finalSize) |
||||
|
{ |
||||
|
double accumulatedV = 0; |
||||
|
var uvFinalSize = CreateUVSize(finalSize); |
||||
|
var lineSize = CreateUVSize(); |
||||
|
int firstChildInLineindex = 0; |
||||
|
for (int index = 0; index < Children.Count; index++) |
||||
|
{ |
||||
|
var child = Children[index]; |
||||
|
var childSize = CreateUVSize(child.DesiredSize); |
||||
|
if (lineSize.U + childSize.U <= uvFinalSize.U) // same line
|
||||
|
{ |
||||
|
lineSize.U += childSize.U; |
||||
|
lineSize.V = Max(lineSize.V, childSize.V); |
||||
|
} |
||||
|
else // moving to next line
|
||||
|
{ |
||||
|
var controlsInLine = GetContolsBetween(firstChildInLineindex, index); |
||||
|
ArrangeLine(accumulatedV, lineSize.V, controlsInLine); |
||||
|
accumulatedV += lineSize.V; |
||||
|
lineSize = childSize; |
||||
|
firstChildInLineindex = index; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (firstChildInLineindex < Children.Count) |
||||
|
{ |
||||
|
var controlsInLine = GetContolsBetween(firstChildInLineindex, Children.Count); |
||||
|
ArrangeLine(accumulatedV, lineSize.V, controlsInLine); |
||||
|
} |
||||
|
return finalSize; |
||||
|
} |
||||
|
|
||||
|
private IEnumerable<IControl> GetContolsBetween(int first, int last) |
||||
|
{ |
||||
|
return Children.Skip(first).Take(last - first); |
||||
|
} |
||||
|
|
||||
|
private void ArrangeLine(double v, double lineV, IEnumerable<IControl> contols) |
||||
|
{ |
||||
|
double u = 0; |
||||
|
bool isHorizontal = (Orientation == Orientation.Horizontal); |
||||
|
foreach (var child in contols) |
||||
|
{ |
||||
|
var childSize = CreateUVSize(child.DesiredSize); |
||||
|
var x = isHorizontal ? u : v; |
||||
|
var y = isHorizontal ? v : u; |
||||
|
var width = isHorizontal ? childSize.U : lineV; |
||||
|
var height = isHorizontal ? lineV : childSize.U; |
||||
|
child.Arrange(new Rect(x, y, width, height)); |
||||
|
u += childSize.U; |
||||
|
} |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// Used to not not write sepearate code for horizontal and vertical orientation.
|
||||
|
/// U is direction in line. (x if orientation is horizontal)
|
||||
|
/// V is direction of lines. (y if orientation is horizonral)
|
||||
|
/// </summary>
|
||||
|
[DebuggerDisplay("U = {U} V = {V}")] |
||||
|
private struct UVSize |
||||
|
{ |
||||
|
private readonly Orientation _orientation; |
||||
|
|
||||
|
internal double U; |
||||
|
|
||||
|
internal double V; |
||||
|
|
||||
|
private UVSize(Orientation orientation, double width, double height) |
||||
|
{ |
||||
|
U = V = 0d; |
||||
|
_orientation = orientation; |
||||
|
Width = width; |
||||
|
Height = height; |
||||
|
} |
||||
|
|
||||
|
internal UVSize(Orientation orientation, Size size) |
||||
|
: this(orientation, size.Width, size.Height) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
internal UVSize(Orientation orientation) |
||||
|
{ |
||||
|
U = V = 0d; |
||||
|
_orientation = orientation; |
||||
|
} |
||||
|
|
||||
|
private double Width |
||||
|
{ |
||||
|
get { return (_orientation == Orientation.Horizontal ? U : V); } |
||||
|
set |
||||
|
{ |
||||
|
if (_orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
U = value; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
V = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private double Height |
||||
|
{ |
||||
|
get { return (_orientation == Orientation.Horizontal ? V : U); } |
||||
|
set |
||||
|
{ |
||||
|
if (_orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
V = value; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
U = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Size ToSize() |
||||
|
{ |
||||
|
return new Size(Width, Height); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
// 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 WrapPanelTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Lays_Out_Horizontally_On_Sepatate_Lines() |
||||
|
{ |
||||
|
var target = new WrapPanel() |
||||
|
{ |
||||
|
Width = 100, |
||||
|
Children = new Controls |
||||
|
{ |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
target.Measure(Size.Infinity); |
||||
|
target.Arrange(new Rect(target.DesiredSize)); |
||||
|
|
||||
|
Assert.Equal(new Size(100, 100), target.Bounds.Size); |
||||
|
Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds); |
||||
|
Assert.Equal(new Rect(0, 50, 100, 50), target.Children[1].Bounds); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Lays_Out_Horizontally_On_A_Single_Line() |
||||
|
{ |
||||
|
var target = new WrapPanel() |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Children = new Controls |
||||
|
{ |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
target.Measure(Size.Infinity); |
||||
|
target.Arrange(new Rect(target.DesiredSize)); |
||||
|
|
||||
|
Assert.Equal(new Size(200, 50), target.Bounds.Size); |
||||
|
Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds); |
||||
|
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Lays_Out_Vertically_Children_On_A_Single_Line() |
||||
|
{ |
||||
|
var target = new WrapPanel() |
||||
|
{ |
||||
|
Orientation = Orientation.Vertical, |
||||
|
Height = 120, |
||||
|
Children = new Controls |
||||
|
{ |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
target.Measure(Size.Infinity); |
||||
|
target.Arrange(new Rect(target.DesiredSize)); |
||||
|
|
||||
|
Assert.Equal(new Size(100, 120), target.Bounds.Size); |
||||
|
Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds); |
||||
|
Assert.Equal(new Rect(0, 50, 100, 50), target.Children[1].Bounds); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Lays_Out_Vertically_On_Sepatate_Lines() |
||||
|
{ |
||||
|
var target = new WrapPanel() |
||||
|
{ |
||||
|
Orientation = Orientation.Vertical, |
||||
|
Height = 60, |
||||
|
Children = new Controls |
||||
|
{ |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
new Border { Height = 50, Width = 100 }, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
target.Measure(Size.Infinity); |
||||
|
target.Arrange(new Rect(target.DesiredSize)); |
||||
|
|
||||
|
Assert.Equal(new Size(200, 60), target.Bounds.Size); |
||||
|
Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds); |
||||
|
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue