diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml
index bb359d9d3b..d51e61500b 100644
--- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml
+++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml
@@ -174,6 +174,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
(x => x.Content, new TemplateLoader());
mapping.Map(x => x.Content, new TemplateLoader());
mapping.Map(x => x.Content, new TemplateLoader());
+ mapping.Map(x => x.Content, new TemplateLoader());
var assembler = new ObjectAssembler(wiringContext, new TopDownValueContext(), objectAssemblerSettings);
_objectAssembler = new TemplateHostingObjectAssembler(assembler, mapping);
diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
index e598455715..60046e001c 100644
--- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
+++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
@@ -242,6 +242,7 @@
+
diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/ItemsPanelTemplate.cs
new file mode 100644
index 0000000000..8b79c125ea
--- /dev/null
+++ b/src/Markup/Perspex.Markup.Xaml/Templates/ItemsPanelTemplate.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Perspex.Controls;
+using Perspex.Metadata;
+
+namespace Perspex.Markup.Xaml.Templates
+{
+ public class ItemsPanelTemplate : ITemplate
+ {
+ [Content]
+ public TemplateContent Content { get; set; }
+
+ public IPanel Build()
+ {
+ return (IPanel)Content.Load();
+ }
+ }
+}
diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj
index 31542f9b7d..9fb34fb553 100644
--- a/src/Perspex.Controls/Perspex.Controls.csproj
+++ b/src/Perspex.Controls/Perspex.Controls.csproj
@@ -162,6 +162,7 @@
+
diff --git a/src/Perspex.Controls/WrapPanel.cs b/src/Perspex.Controls/WrapPanel.cs
new file mode 100644
index 0000000000..fca4714fd5
--- /dev/null
+++ b/src/Perspex.Controls/WrapPanel.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ internal class WrapPanel : Panel, INavigableContainer
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly PerspexProperty OrientationProperty =
+ PerspexProperty.Register(nameof(Orientation), defaultValue: Orientation.Horizontal);
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ static WrapPanel()
+ {
+ AffectsMeasure(OrientationProperty);
+ }
+
+ ///
+ /// Gets or sets the orientation in which child controls will be layed out.
+ ///
+ public Orientation Orientation { get { return GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } }
+
+ ///
+ /// Gets the next control in the specified direction.
+ ///
+ /// The movement direction.
+ /// The control from which movement begins.
+ /// The control.
+ 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);
+
+ ///
+ 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();
+ }
+
+ ///
+ 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 GetContolsBetween(int first, int last)
+ {
+ return Children.Skip(first).Take(last - first);
+ }
+
+ private void ArrangeLine(double v, double lineV, IEnumerable 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;
+ }
+ }
+ ///
+ /// 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)
+ ///
+ [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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Perspex.Styling/Styling/Selector.cs b/src/Perspex.Styling/Styling/Selector.cs
index 3ae9feda4c..889b343cfb 100644
--- a/src/Perspex.Styling/Styling/Selector.cs
+++ b/src/Perspex.Styling/Styling/Selector.cs
@@ -10,7 +10,7 @@ namespace Perspex.Styling
/// A selector in a .
///
///
- /// Selectors represented in markup using a CSS-like syntax, e.g. "Button < .dark" which
+ /// Selectors represented in markup using a CSS-like syntax, e.g. "Button > .dark" which
/// means "A child of a Button with the 'dark' class applied. The preceeding example would be
/// stored in 3 objects, linked by the property:
///
@@ -21,7 +21,7 @@ namespace Perspex.Styling
///
///
/// -
- /// <
+ /// >
///
/// A selector that selects a child of the previous selector.
///
diff --git a/src/Perspex.Themes.Default/TabStrip.paml b/src/Perspex.Themes.Default/TabStrip.paml
index 20927c6a77..a9df7c0ca7 100644
--- a/src/Perspex.Themes.Default/TabStrip.paml
+++ b/src/Perspex.Themes.Default/TabStrip.paml
@@ -6,9 +6,13 @@
ItemsPanel="{TemplateBinding ItemsPanel}"/>
+
+
+
+
+
-
\ No newline at end of file
diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
index 912bcd534a..f299a47e6d 100644
--- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
+++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
@@ -120,6 +120,7 @@
+
diff --git a/tests/Perspex.Controls.UnitTests/WrapPanelTests.cs b/tests/Perspex.Controls.UnitTests/WrapPanelTests.cs
new file mode 100644
index 0000000000..6f485b3e03
--- /dev/null
+++ b/tests/Perspex.Controls.UnitTests/WrapPanelTests.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file