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