diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 12440b4106..945b9cb1b0 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -221,6 +221,19 @@ + + + + + + + + + + + + + diff --git a/src/Perspex.Controls/GridSplitter.cs b/src/Perspex.Controls/GridSplitter.cs index 9a0ef0b988..39e235cda3 100644 --- a/src/Perspex.Controls/GridSplitter.cs +++ b/src/Perspex.Controls/GridSplitter.cs @@ -2,35 +2,159 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Linq; + +using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Input; -using Perspex.Rendering; using Perspex.VisualTree; namespace Perspex.Controls { + /// + /// Represents the control that redistributes space between columns or rows of a Grid control. + /// + /// + /// Unlike WPF GridSplitter, Perspex GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext. + /// public class GridSplitter : Thumb { - private Grid _grid; + /// + /// Defines the property. + /// + public static readonly PerspexProperty OrientationProperty = + PerspexProperty.Register(nameof(Orientation)); + + protected Grid _grid; + + private DefinitionBase _prevDefinition; + + private DefinitionBase _nextDefinition; + + private bool _isResizingColumns; + + private List _definitions; /// - /// Initializes a new instance of the class. + /// Gets or sets the orientation of the GridsSlitter. /// - public GridSplitter() + /// + /// if null, it's inferred from column/row definition (should be auto). + /// + public Orientation? Orientation { + get + { + return GetValue(OrientationProperty); + } + set + { + SetValue(OrientationProperty, value); + } + } + + static GridSplitter() { - Cursor = new Cursor(StandardCursorType.SizeWestEast); + PseudoClass(OrientationProperty, o => o == Perspex.Controls.Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Perspex.Controls.Orientation.Horizontal, ":horizontal"); + } + + private void GetDeltaConstraints(out double min, out double max) + { + double prevDefinitionLen = GetActualLength(_prevDefinition); + double prevDefinitionMin = GetMinLength(_prevDefinition); + double prevDefinitionMax = GetMaxLength(_prevDefinition); + + double nextDefinitionLen = GetActualLength(_nextDefinition); + double nextDefinitionMin = GetMinLength(_nextDefinition); + double nextDefinitionMax = GetMaxLength(_nextDefinition); + // Determine the minimum and maximum the columns can be resized + min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen); + max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin); } protected override void OnDragDelta(VectorEventArgs e) { - int col = GetValue(Grid.ColumnProperty); + var delta = Orientation.Value == Perspex.Controls.Orientation.Vertical ? e.Vector.X : e.Vector.Y; + double max; + double min; + GetDeltaConstraints(out min, out max); + delta = Math.Min(Math.Max(delta, min), max); + foreach (var definition in _definitions) + { + if (definition == _prevDefinition) + { + SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta); + } + else if (definition == _nextDefinition) + { + SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta); + } + else if (IsStar(definition)) + { + SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars. + } + } + } - if (_grid != null && col > 0) + /// + /// If orientation is not set, method automatically calculates orientation based column/row auto size. + /// + private void AutoSetOrientation() + { + if (Orientation.HasValue) { - var size = _grid.ColumnDefinitions[col - 1].ActualWidth + e.Vector.X; - _grid.ColumnDefinitions[col - 1].Width = new GridLength( - Math.Max(0, size), - GridUnitType.Pixel); + return; + } + var column = GetValue(Grid.ColumnProperty); + if (column < _grid.ColumnDefinitions.Count && _grid.ColumnDefinitions[column].Width.IsAuto) + { + Orientation = Perspex.Controls.Orientation.Vertical; + return; + } + var row = GetValue(Grid.RowProperty); + if (row < _grid.RowDefinitions.Count && _grid.RowDefinitions[row].Height.IsAuto) + { + Orientation = Perspex.Controls.Orientation.Horizontal; + return; + } + throw new InvalidOperationException("GridSpliter Should have Orientation, width or height set."); + } + + private double GetActualLength(DefinitionBase definition) + { + var columnDefinition = definition as ColumnDefinition; + return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight; + } + + private double GetMinLength(DefinitionBase definition) + { + var columnDefinition = definition as ColumnDefinition; + return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight; + } + + private double GetMaxLength(DefinitionBase definition) + { + var columnDefinition = definition as ColumnDefinition; + return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight; + } + + private bool IsStar(DefinitionBase definition) + { + var columnDefinition = definition as ColumnDefinition; + return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar; + } + + private void SetLengthInStars(DefinitionBase definition, double value) + { + var columnDefinition = definition as ColumnDefinition; + if (columnDefinition != null) + { + columnDefinition.Width = new GridLength(value, GridUnitType.Star); + } + else + { + ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star); } } @@ -38,6 +162,24 @@ namespace Perspex.Controls { base.OnAttachedToVisualTree(e); _grid = this.GetVisualParent(); + AutoSetOrientation(); + if (Orientation.Value == Perspex.Controls.Orientation.Vertical) + { + Cursor = new Cursor(StandardCursorType.SizeWestEast); + var col = GetValue(Grid.ColumnProperty); + _definitions = _grid.ColumnDefinitions.Cast().ToList(); + _prevDefinition = _definitions[col - 1]; + _nextDefinition = _definitions[col + 1]; + } + else + { + Cursor = new Cursor(StandardCursorType.SizeNorthSouth); + var row = GetValue(Grid.RowProperty); + _definitions = _grid.RowDefinitions.Cast().ToList(); + _prevDefinition = _definitions[row - 1]; + _nextDefinition = _definitions[row + 1]; + } } } } + diff --git a/src/Perspex.Themes.Default/DefaultTheme.paml b/src/Perspex.Themes.Default/DefaultTheme.paml index e3bcf623f1..9d456d4af9 100644 --- a/src/Perspex.Themes.Default/DefaultTheme.paml +++ b/src/Perspex.Themes.Default/DefaultTheme.paml @@ -1,6 +1,5 @@  - diff --git a/src/Perspex.Themes.Default/GridSplitter.paml b/src/Perspex.Themes.Default/GridSplitter.paml index ee0757a3f5..6c5f936ec2 100644 --- a/src/Perspex.Themes.Default/GridSplitter.paml +++ b/src/Perspex.Themes.Default/GridSplitter.paml @@ -1,8 +1,51 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Perspex.Controls.UnitTests/GridSplitterTests.cs b/tests/Perspex.Controls.UnitTests/GridSplitterTests.cs new file mode 100644 index 0000000000..2fd2c71034 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/GridSplitterTests.cs @@ -0,0 +1,106 @@ +using Moq; + +using Perspex.Controls.Primitives; +using Perspex.Input; +using Perspex.Platform; + +using Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class GridSplitterTests + { + [Fact] + public void Vertical_Stays_Within_Constrains() + { + var cursorFactoryImpl = new Mock(); + PerspexLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); + + var control1 = new Border { [Grid.ColumnProperty] = 0 }; + var splitter = new GridSplitter { [Grid.ColumnProperty] = 1}; + var control2 = new Border { [Grid.ColumnProperty] = 2 }; + + var columnDefenitions = new ColumnDefinitions() + { + new ColumnDefinition(1, GridUnitType.Star) {MinWidth = 10, MaxWidth = 190}, + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(1, GridUnitType.Star) {MinWidth = 80, MaxWidth = 120}, + }; + var grid = new Grid() + { + ColumnDefinitions = columnDefenitions, + Children = new Controls() + { + control1, splitter, control2 + } + }; + + var root = new TestRoot { Child = grid }; + Assert.Equal(splitter.Orientation, Orientation.Vertical); + + root.Measure(new Size(200, 100)); + root.Arrange(new Rect(0, 0, 200, 100)); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(-100,0) + }); + Assert.Equal(columnDefenitions[0].Width, new GridLength(80,GridUnitType.Star)); + Assert.Equal(columnDefenitions[2].Width, new GridLength(120,GridUnitType.Star)); + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(100, 0) + }); + Assert.Equal(columnDefenitions[0].Width, new GridLength(120, GridUnitType.Star)); + Assert.Equal(columnDefenitions[2].Width, new GridLength(80, GridUnitType.Star)); + } + + [Fact] + public void Horizontal_Stays_Within_Constrains() + { + var cursorFactoryImpl = new Mock(); + PerspexLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); + + var control1 = new Border { [Grid.RowProperty] = 0 }; + var splitter = new GridSplitter { [Grid.RowProperty] = 1 }; + var control2 = new Border { [Grid.RowProperty] = 2 }; + + var rowDefenitions = new RowDefinitions() + { + new RowDefinition(1, GridUnitType.Star) {MinHeight = 70, MaxHeight = 110}, + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140}, + }; + var grid = new Grid() + { + RowDefinitions = rowDefenitions, + Children = new Controls() + { + control1, splitter, control2 + } + }; + + var root = new TestRoot { Child = grid }; + Assert.Equal(splitter.Orientation, Orientation.Horizontal); + root.Measure(new Size(100, 200)); + root.Arrange(new Rect(0, 0, 100, 200)); + + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, -100) + }); + Assert.Equal(rowDefenitions[0].Height, new GridLength(70, GridUnitType.Star)); + Assert.Equal(rowDefenitions[2].Height, new GridLength(130, GridUnitType.Star)); + splitter.RaiseEvent(new VectorEventArgs + { + RoutedEvent = Thumb.DragDeltaEvent, + Vector = new Vector(0, 100) + }); + Assert.Equal(rowDefenitions[0].Height, new GridLength(110, GridUnitType.Star)); + Assert.Equal(rowDefenitions[2].Height, new GridLength(90, GridUnitType.Star)); + } + } +} \ 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 2ec1569e11..5b33890f2a 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -83,6 +83,7 @@ +