diff --git a/components/Sizers/OpenSolution.bat b/components/Sizers/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Sizers/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Sizers/samples/ContentSizer.md b/components/Sizers/samples/ContentSizer.md new file mode 100644 index 00000000..6c206ee8 --- /dev/null +++ b/components/Sizers/samples/ContentSizer.md @@ -0,0 +1,26 @@ +--- +title: ContentSizer +author: mhawker +description: The ContentSizer is a control which can be used to resize any element, usually its parent. +keywords: ContentSizer, SizerBase, Control, Layout, Expander, Splitter +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# ContentSizer + +The ContentSizer is a control which can be used to resize any element, usually its parent. If you are using a `Grid`, use [GridSplitter](GridSplitter.md) instead. + +The main use-case for a ContentSizer is to create an expandable shelf for your application. This allows the `Expander` itself to remember its opening/closing sizes. + +A GridSplitter would be insufficient as it would force the grid to remember the row size and maintain its position when the `Expander` collapses. + +> [!SAMPLE ContentSizerTopShelfPage] + +The following example shows how to use the ContentSizer to create a left-side shelf; however, this scenario can also be accomplished with a `GridSplitter`. + +> [!SAMPLE ContentSizerLeftShelfPage] diff --git a/components/Sizers/samples/ContentSizerLeftShelfPage.xaml b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml new file mode 100644 index 00000000..7bb1de95 --- /dev/null +++ b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs new file mode 100644 index 00000000..6c6a375a --- /dev/null +++ b/components/Sizers/samples/ContentSizerLeftShelfPage.xaml.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SizersExperiment.Samples; + +[ToolkitSample(id: nameof(ContentSizerLeftShelfPage), "Left-side Shelf", description: "Shows how to create an expandable shelf on the left-side of your app.")] +public sealed partial class ContentSizerLeftShelfPage : Page +{ + public ContentSizerLeftShelfPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/ContentSizerTopShelfPage.xaml b/components/Sizers/samples/ContentSizerTopShelfPage.xaml new file mode 100644 index 00000000..2f2b4b0c --- /dev/null +++ b/components/Sizers/samples/ContentSizerTopShelfPage.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs b/components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs new file mode 100644 index 00000000..3142311f --- /dev/null +++ b/components/Sizers/samples/ContentSizerTopShelfPage.xaml.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SizersExperiment.Samples; + +[ToolkitSample(id: nameof(ContentSizerTopShelfPage), "Top Shelf", description: "Shows how to create an expandable shelf on the top of your app.")] +public sealed partial class ContentSizerTopShelfPage : Page +{ + public ContentSizerTopShelfPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/Dependencies.props b/components/Sizers/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Sizers/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/GridSplitter.md b/components/Sizers/samples/GridSplitter.md new file mode 100644 index 00000000..0a5207c3 --- /dev/null +++ b/components/Sizers/samples/GridSplitter.md @@ -0,0 +1,21 @@ +--- +title: GridSplitter +author: mhawker +description:The GridSplitter control provides an easy-to-use Splitter that redistributes space between columns or rows of a Grid Control. +keywords: ContentSizer, SizerBase, Control, Layout, Expander +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# GridSplitter + +The control automatically detects the targeted columns/rows to resize, while dragging the control it starts to resize the columns/rows and redistributes space between columns/rows, +you can manually specify the `ResizeDirection` (`Auto` / `Column` / `Row`) and the `ResizeBehavior` to select which columns/rows to resize. + +`GridSplitter` control will resize the targeted rows or columns + +> [!SAMPLE GridSplitterPage] diff --git a/components/Sizers/samples/GridSplitterPage.xaml b/components/Sizers/samples/GridSplitterPage.xaml new file mode 100644 index 00000000..bba2d2ec --- /dev/null +++ b/components/Sizers/samples/GridSplitterPage.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/GridSplitterPage.xaml.cs b/components/Sizers/samples/GridSplitterPage.xaml.cs new file mode 100644 index 00000000..bbe748ef --- /dev/null +++ b/components/Sizers/samples/GridSplitterPage.xaml.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SizersExperiment.Samples; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +[ToolkitSample(id: nameof(GridSplitterPage), "GridSplitter Example", description: "Splitter that redistributes space between columns or rows of a Grid Control")] +public sealed partial class GridSplitterPage : Page +{ + public GridSplitterPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/PropertySizer.md b/components/Sizers/samples/PropertySizer.md new file mode 100644 index 00000000..90c59c06 --- /dev/null +++ b/components/Sizers/samples/PropertySizer.md @@ -0,0 +1,24 @@ +--- +title: PropertySizer +author: mhawker +description: The PropertySizer is a control which can be used to manipulate the value of another double based property. +keywords: PropertySizer, SizerBase, Control, Layout, NavigationView, Splitter +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# PropertySizer + +The PropertySizer is a control which can be used to manipulate the value of another double based property. For instance manipulating the `OpenPaneLength` of a `NavigationView` control. If you are using a `Grid`, use `GridSplitter` instead. + +# Examples + +The main use-case is for `PropertySizer` to allow you to manipulate the `OpenPaneLength` property of a `NavigationView` control to create a user customizable size shelf. This is handy when using `NavigationView` with a tree of items that represents some project or folder structure for your application. + +Both `GridSplitter` and `ContentSizer` are insufficient as they would force the `NavigationView` to a specific size and not allow it to remember its size when it expands or collapses. + +> [!SAMPLE PropertySizerNavigationViewPage] diff --git a/components/Sizers/samples/PropertySizerNavigationViewPage.xaml b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml new file mode 100644 index 00000000..b8f17c61 --- /dev/null +++ b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs new file mode 100644 index 00000000..b46da254 --- /dev/null +++ b/components/Sizers/samples/PropertySizerNavigationViewPage.xaml.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SizersExperiment.Samples; + +[ToolkitSample(id: nameof(PropertySizerNavigationViewPage), "NavigationView Shelf", description: "Shows how to create an expandable shelf using a NavigationView and PropertySizer.")] +public sealed partial class PropertySizerNavigationViewPage : Page +{ + public PropertySizerNavigationViewPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/samples/SizerBase.Samples.csproj b/components/Sizers/samples/SizerBase.Samples.csproj new file mode 100644 index 00000000..6f0b9aeb --- /dev/null +++ b/components/Sizers/samples/SizerBase.Samples.csproj @@ -0,0 +1,8 @@ + + + Sizers + + + + + diff --git a/components/Sizers/samples/SizerControls.md b/components/Sizers/samples/SizerControls.md new file mode 100644 index 00000000..fc3912c4 --- /dev/null +++ b/components/Sizers/samples/SizerControls.md @@ -0,0 +1,30 @@ +--- +title: Sizer Controls +author: mhawker +description: The Sizer controls allow users to resize various parts of your UI easily in a consistent fashion. +keywords: GridSplitter, ContentSizer, PropertySizer, SizerBase, Control, Layout, Expander, Grid, Splitter +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 96 +issue-id: 101 +--- + +# Sizer Controls + +The Sizer controls consist of the following: + +- GridSplitter +- ContentSizer +- PropertySizer + +They each provide an ability for your users to manipulate different parts of your UI experiences. + +This document provides information about common settings you can set on any of these controls. + +## Custom Mouse Cursor + +You may want to change the cursor that is shown when hovering over your element like this: + +> [!SAMPLE SizerCursorPage] diff --git a/components/Sizers/samples/SizerCursorPage.xaml b/components/Sizers/samples/SizerCursorPage.xaml new file mode 100644 index 00000000..d06b8ed3 --- /dev/null +++ b/components/Sizers/samples/SizerCursorPage.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/components/Sizers/samples/SizerCursorPage.xaml.cs b/components/Sizers/samples/SizerCursorPage.xaml.cs new file mode 100644 index 00000000..b12501ea --- /dev/null +++ b/components/Sizers/samples/SizerCursorPage.xaml.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SizersExperiment.Samples; + +[ToolkitSample(id: nameof(SizerCursorPage), "Custom Mouse Cursor", description: "Shows how to change the cursor of a Sizer control.")] +public sealed partial class SizerCursorPage : Page +{ + public SizerCursorPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/src/AdditionalAssemblyInfo.cs b/components/Sizers/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..dc2a92d0 --- /dev/null +++ b/components/Sizers/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("SizerBase.Tests.Uwp")] +[assembly: InternalsVisibleTo("SizerBase.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj new file mode 100644 index 00000000..d3c03e39 --- /dev/null +++ b/components/Sizers/src/CommunityToolkit.WinUI.Controls.Sizers.csproj @@ -0,0 +1,17 @@ + + + Sizers + This package contains SizerBase. + 8.0.0-beta.1 + + + CommunityToolkit.WinUI.Controls.SizersRns + + + + + + + + + diff --git a/components/Sizers/src/ContentSizer/ContentSizer.Events.cs b/components/Sizers/src/ContentSizer/ContentSizer.Events.cs new file mode 100644 index 00000000..0778459a --- /dev/null +++ b/components/Sizers/src/ContentSizer/ContentSizer.Events.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI; + +namespace CommunityToolkit.WinUI.Controls; + +// Events for ContentSizer. +public partial class ContentSizer +{ + /// + protected override void OnLoaded(RoutedEventArgs e) + { + if (TargetControl == null) + { + TargetControl = this.FindAscendant(); + } + } + + private double _currentSize; + + /// + protected override void OnDragStarting() + { + if (TargetControl != null) + { + _currentSize = + Orientation == Orientation.Vertical ? + TargetControl.ActualWidth : + TargetControl.ActualHeight; + } + } + + /// + protected override bool OnDragHorizontal(double horizontalChange) + { + if (TargetControl == null) + { + return true; + } + + horizontalChange = IsDragInverted ? -horizontalChange : horizontalChange; + + if (!IsValidWidth(TargetControl, _currentSize + horizontalChange, ActualWidth)) + { + return false; + } + + TargetControl.Width = _currentSize + horizontalChange; + + return true; + } + + /// + protected override bool OnDragVertical(double verticalChange) + { + if (TargetControl == null) + { + return false; + } + + verticalChange = IsDragInverted ? -verticalChange : verticalChange; + + if (!IsValidHeight(TargetControl, _currentSize + verticalChange, ActualHeight)) + { + return false; + } + + TargetControl.Height = _currentSize + verticalChange; + + return true; + } +} diff --git a/components/Sizers/src/ContentSizer/ContentSizer.Properties.cs b/components/Sizers/src/ContentSizer/ContentSizer.Properties.cs new file mode 100644 index 00000000..4bcf58aa --- /dev/null +++ b/components/Sizers/src/ContentSizer/ContentSizer.Properties.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +// Properties for ContentSizer. +public partial class ContentSizer +{ + /// + /// Gets or sets a value indicating whether the control is resizing in the opposite direction. + /// + public bool IsDragInverted + { + get { return (bool)GetValue(IsDragInvertedProperty); } + set { SetValue(IsDragInvertedProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsDragInvertedProperty = + DependencyProperty.Register(nameof(IsDragInverted), typeof(bool), typeof(ContentSizer), new PropertyMetadata(false)); + + /// + /// Gets or sets the control that the is resizing. Be default, this will be the visual ancestor of the . + /// + public FrameworkElement? TargetControl + { + get { return (FrameworkElement?)GetValue(TargetControlProperty); } + set { SetValue(TargetControlProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetControlProperty = + DependencyProperty.Register(nameof(TargetControl), typeof(FrameworkElement), typeof(ContentSizer), new PropertyMetadata(null, OnTargetControlChanged)); + + private static void OnTargetControlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // TODO: Should we do this after the TargetControl is Loaded? (And use ActualWidth?) + // Or should we just do it in the manipulation event if Width is null? + + // Check if our width can be manipulated + if (d is SizerBase splitterBase && e.NewValue is FrameworkElement element) + { + // TODO: For Auto ResizeDirection we might want to do detection logic (TBD) here first? + if (splitterBase.Orientation != Orientation.Horizontal && double.IsNaN(element.Width)) + { + // We need to set the Width or Height somewhere, + // as if it's NaN we won't be able to manipulate it. + element.Width = element.DesiredSize.Width; + } + + if (splitterBase.Orientation != Orientation.Vertical && double.IsNaN(element.Height)) + { + element.Height = element.DesiredSize.Height; + } + } + } +} diff --git a/components/Sizers/src/ContentSizer/ContentSizer.cs b/components/Sizers/src/ContentSizer/ContentSizer.cs new file mode 100644 index 00000000..3de9fb05 --- /dev/null +++ b/components/Sizers/src/ContentSizer/ContentSizer.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// The is a control which can be used to resize any element, usually its parent. If you are using a , use instead. +/// +public partial class ContentSizer : SizerBase +{ +} diff --git a/components/Sizers/src/Dependencies.props b/components/Sizers/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Sizers/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Sizers/src/GridSplitter/GridSplitter.Data.cs b/components/Sizers/src/GridSplitter/GridSplitter.Data.cs new file mode 100644 index 00000000..d525d1d4 --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Data.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GridSplitter +{ + /// + /// Enum to indicate whether GridSplitter resizes Columns or Rows + /// + public enum GridResizeDirection + { + /// + /// Determines whether to resize rows or columns based on its Alignment and + /// width compared to height + /// + Auto, + + /// + /// Resize columns when dragging Splitter. + /// + Columns, + + /// + /// Resize rows when dragging Splitter. + /// + Rows + } + + /// + /// Enum to indicate what Columns or Rows the GridSplitter resizes + /// + public enum GridResizeBehavior + { + /// + /// Determine which columns or rows to resize based on its Alignment. + /// + BasedOnAlignment, + + /// + /// Resize the current and next Columns or Rows. + /// + CurrentAndNext, + + /// + /// Resize the previous and current Columns or Rows. + /// + PreviousAndCurrent, + + /// + /// Resize the previous and next Columns or Rows. + /// + PreviousAndNext + } +} diff --git a/components/Sizers/src/GridSplitter/GridSplitter.Events.cs b/components/Sizers/src/GridSplitter/GridSplitter.Events.cs new file mode 100644 index 00000000..508472f7 --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Events.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GridSplitter +{ + /// + protected override void OnLoaded(RoutedEventArgs e) + { + _resizeDirection = GetResizeDirection(); + Orientation = _resizeDirection == GridResizeDirection.Rows ? + Orientation.Horizontal : Orientation.Vertical; + _resizeBehavior = GetResizeBehavior(); + } + + private double _currentSize; + private double _siblingSize; + + /// + protected override void OnDragStarting() + { + _resizeDirection = GetResizeDirection(); + Orientation = _resizeDirection == GridResizeDirection.Rows ? + Orientation.Horizontal : Orientation.Vertical; + _resizeBehavior = GetResizeBehavior(); + + // Record starting points + if (Orientation == Orientation.Horizontal) + { + _currentSize = CurrentRow?.ActualHeight ?? -1; + _siblingSize = SiblingRow?.ActualHeight ?? -1; + } + else + { + _currentSize = CurrentColumn?.ActualWidth ?? -1; + _siblingSize = SiblingColumn?.ActualWidth ?? -1; + } + } + + /// + protected override bool OnDragVertical(double verticalChange) + { + if (CurrentRow == null || SiblingRow == null || Resizable == null) + { + return false; + } + + var currentChange = _currentSize + verticalChange; + var siblingChange = _siblingSize + (verticalChange * -1); // sibling moves opposite + + // if current row has fixed height then resize it + if (!IsStarRow(CurrentRow)) + { + // No need to check for the row Min height because it is automatically respected + return SetRowHeight(CurrentRow, currentChange, GridUnitType.Pixel); + } + + // if sibling row has fixed width then resize it + else if (!IsStarRow(SiblingRow)) + { + // Would adding to this column make the current column violate the MinWidth? + if (IsValidRowHeight(CurrentRow, currentChange) == false) + { + return false; + } + + return SetRowHeight(SiblingRow, siblingChange, GridUnitType.Pixel); + } + + // if both row haven't fixed height (auto *) + else + { + // change current row height to the new height with respecting the auto + // change sibling row height to the new height relative to current row + // respect the other star row height by setting it's height to it's actual height with stars + + // We need to validate current and sibling height to not cause any unexpected behavior + if (!IsValidRowHeight(CurrentRow, currentChange) || + !IsValidRowHeight(SiblingRow, siblingChange)) + { + return false; + } + + foreach (var rowDefinition in Resizable.RowDefinitions) + { + if (rowDefinition == CurrentRow) + { + SetRowHeight(CurrentRow, currentChange, GridUnitType.Star); + } + else if (rowDefinition == SiblingRow) + { + SetRowHeight(SiblingRow, siblingChange, GridUnitType.Star); + } + else if (IsStarRow(rowDefinition)) + { + rowDefinition.Height = new GridLength(rowDefinition.ActualHeight, GridUnitType.Star); + } + } + + return true; + } + } + + /// + protected override bool OnDragHorizontal(double horizontalChange) + { + if (CurrentColumn == null || SiblingColumn == null || Resizable == null) + { + return false; + } + + var currentChange = _currentSize + horizontalChange; + var siblingChange = _siblingSize + (horizontalChange * -1); // sibling moves opposite + + // if current column has fixed width then resize it + if (!IsStarColumn(CurrentColumn)) + { + // No need to check for the Column Min width because it is automatically respected + return SetColumnWidth(CurrentColumn, currentChange, GridUnitType.Pixel); + } + + // if sibling column has fixed width then resize it + else if (!IsStarColumn(SiblingColumn)) + { + // Would adding to this column make the current column violate the MinWidth? + if (IsValidColumnWidth(CurrentColumn, currentChange) == false) + { + return false; + } + + return SetColumnWidth(SiblingColumn, siblingChange, GridUnitType.Pixel); + } + + // if both column haven't fixed width (auto *) + else + { + // change current column width to the new width with respecting the auto + // change sibling column width to the new width relative to current column + // respect the other star column width by setting it's width to it's actual width with stars + + // We need to validate current and sibling width to not cause any unexpected behavior + if (!IsValidColumnWidth(CurrentColumn, currentChange) || + !IsValidColumnWidth(SiblingColumn, siblingChange)) + { + return false; + } + + foreach (var columnDefinition in Resizable.ColumnDefinitions) + { + if (columnDefinition == CurrentColumn) + { + SetColumnWidth(CurrentColumn, currentChange, GridUnitType.Star); + } + else if (columnDefinition == SiblingColumn) + { + SetColumnWidth(SiblingColumn, siblingChange, GridUnitType.Star); + } + else if (IsStarColumn(columnDefinition)) + { + columnDefinition.Width = new GridLength(columnDefinition.ActualWidth, GridUnitType.Star); + } + } + + return true; + } + } +} diff --git a/components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs b/components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs new file mode 100644 index 00000000..361fd77f --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Helpers.cs @@ -0,0 +1,245 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GridSplitter +{ + private static bool IsStarColumn(ColumnDefinition definition) + { + return ((GridLength)definition.GetValue(ColumnDefinition.WidthProperty)).IsStar; + } + + private static bool IsStarRow(RowDefinition definition) + { + return ((GridLength)definition.GetValue(RowDefinition.HeightProperty)).IsStar; + } + + private bool SetColumnWidth(ColumnDefinition columnDefinition, double newWidth, GridUnitType unitType) + { + var minWidth = columnDefinition.MinWidth; + if (!double.IsNaN(minWidth) && newWidth < minWidth) + { + newWidth = minWidth; + } + + var maxWidth = columnDefinition.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + newWidth = maxWidth; + } + + if (newWidth > ActualWidth) + { + columnDefinition.Width = new GridLength(newWidth, unitType); + return true; + } + + return false; + } + + private bool IsValidColumnWidth(ColumnDefinition columnDefinition, double newWidth) + { + var minWidth = columnDefinition.MinWidth; + if (!double.IsNaN(minWidth) && newWidth < minWidth) + { + return false; + } + + var maxWidth = columnDefinition.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + return false; + } + + if (newWidth <= ActualWidth) + { + return false; + } + + return true; + } + + private bool SetRowHeight(RowDefinition rowDefinition, double newHeight, GridUnitType unitType) + { + var minHeight = rowDefinition.MinHeight; + if (!double.IsNaN(minHeight) && newHeight < minHeight) + { + newHeight = minHeight; + } + + var maxWidth = rowDefinition.MaxHeight; + if (!double.IsNaN(maxWidth) && newHeight > maxWidth) + { + newHeight = maxWidth; + } + + if (newHeight > ActualHeight) + { + rowDefinition.Height = new GridLength(newHeight, unitType); + return true; + } + + return false; + } + + private bool IsValidRowHeight(RowDefinition rowDefinition, double newHeight) + { + var minHeight = rowDefinition.MinHeight; + if (!double.IsNaN(minHeight) && newHeight < minHeight) + { + return false; + } + + var maxHeight = rowDefinition.MaxHeight; + if (!double.IsNaN(maxHeight) && newHeight > maxHeight) + { + return false; + } + + if (newHeight <= ActualHeight) + { + return false; + } + + return true; + } + + // Return the targeted Column based on the resize behavior + private int GetTargetedColumn() + { + var currentIndex = Grid.GetColumn(TargetControl); + return GetTargetIndex(currentIndex); + } + + // Return the sibling Row based on the resize behavior + private int GetTargetedRow() + { + var currentIndex = Grid.GetRow(TargetControl); + return GetTargetIndex(currentIndex); + } + + // Return the sibling Column based on the resize behavior + private int GetSiblingColumn() + { + var currentIndex = Grid.GetColumn(TargetControl); + return GetSiblingIndex(currentIndex); + } + + // Return the sibling Row based on the resize behavior + private int GetSiblingRow() + { + var currentIndex = Grid.GetRow(TargetControl); + return GetSiblingIndex(currentIndex); + } + + // Gets index based on resize behavior for first targeted row/column + private int GetTargetIndex(int currentIndex) + { + switch (_resizeBehavior) + { + case GridResizeBehavior.CurrentAndNext: + return currentIndex; + case GridResizeBehavior.PreviousAndNext: + return currentIndex - 1; + case GridResizeBehavior.PreviousAndCurrent: + return currentIndex - 1; + default: + return -1; + } + } + + // Gets index based on resize behavior for second targeted row/column + private int GetSiblingIndex(int currentIndex) + { + switch (_resizeBehavior) + { + case GridResizeBehavior.CurrentAndNext: + return currentIndex + 1; + case GridResizeBehavior.PreviousAndNext: + return currentIndex + 1; + case GridResizeBehavior.PreviousAndCurrent: + return currentIndex; + default: + return -1; + } + } + + // Checks the control alignment and Width/Height to detect the control resize direction columns/rows + private GridResizeDirection GetResizeDirection() + { + GridResizeDirection direction = ResizeDirection; + + if (direction == GridResizeDirection.Auto) + { + // When HorizontalAlignment is Left, Right or Center, resize Columns + if (HorizontalAlignment != HorizontalAlignment.Stretch) + { + direction = GridResizeDirection.Columns; + } + + // When VerticalAlignment is Top, Bottom or Center, resize Rows + else if (VerticalAlignment != VerticalAlignment.Stretch) + { + direction = GridResizeDirection.Rows; + } + + // Check Width vs Height + else if (ActualWidth <= ActualHeight) + { + direction = GridResizeDirection.Columns; + } + else + { + direction = GridResizeDirection.Rows; + } + } + + return direction; + } + + // Get the resize behavior (Which columns/rows should be resized) based on alignment and Direction + private GridResizeBehavior GetResizeBehavior() + { + GridResizeBehavior resizeBehavior = ResizeBehavior; + + if (resizeBehavior == GridResizeBehavior.BasedOnAlignment) + { + if (_resizeDirection == GridResizeDirection.Columns) + { + switch (HorizontalAlignment) + { + case HorizontalAlignment.Left: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case HorizontalAlignment.Right: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; + } + } + + // resize direction is vertical + else + { + switch (VerticalAlignment) + { + case VerticalAlignment.Top: + resizeBehavior = GridResizeBehavior.PreviousAndCurrent; + break; + case VerticalAlignment.Bottom: + resizeBehavior = GridResizeBehavior.CurrentAndNext; + break; + default: + resizeBehavior = GridResizeBehavior.PreviousAndNext; + break; + } + } + } + + return resizeBehavior; + } +} diff --git a/components/Sizers/src/GridSplitter/GridSplitter.Properties.cs b/components/Sizers/src/GridSplitter/GridSplitter.Properties.cs new file mode 100644 index 00000000..ec211c9e --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.Properties.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GridSplitter +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ResizeDirectionProperty + = DependencyProperty.Register( + nameof(ResizeDirection), + typeof(GridResizeDirection), + typeof(GridSplitter), + new PropertyMetadata(GridResizeDirection.Auto, OnResizeDirectionPropertyChanged)); + + private static void OnResizeDirectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is GridSplitter splitter && e.NewValue is GridResizeDirection direction && + direction != GridResizeDirection.Auto) + { + // Update base classes property based on specific polyfill for GridSplitter + splitter.Orientation = + direction == GridResizeDirection.Rows ? + Orientation.Horizontal : + Orientation.Vertical; + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ResizeBehaviorProperty + = DependencyProperty.Register( + nameof(ResizeBehavior), + typeof(GridResizeBehavior), + typeof(GridSplitter), + new PropertyMetadata(GridResizeBehavior.BasedOnAlignment)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ParentLevelProperty + = DependencyProperty.Register( + nameof(ParentLevel), + typeof(int), + typeof(GridSplitter), + new PropertyMetadata(default(int))); + + /// + /// Gets or sets whether the Splitter resizes the Columns, Rows, or Both. + /// + public GridResizeDirection ResizeDirection + { + get { return (GridResizeDirection)GetValue(ResizeDirectionProperty); } + set { SetValue(ResizeDirectionProperty, value); } + } + + /// + /// Gets or sets which Columns or Rows the Splitter resizes. + /// + public GridResizeBehavior ResizeBehavior + { + get { return (GridResizeBehavior)GetValue(ResizeBehaviorProperty); } + set { SetValue(ResizeBehaviorProperty, value); } + } + + /// + /// Gets or sets the level of the parent grid to resize + /// + public int ParentLevel + { + get { return (int)GetValue(ParentLevelProperty); } + set { SetValue(ParentLevelProperty, value); } + } +} diff --git a/components/Sizers/src/GridSplitter/GridSplitter.cs b/components/Sizers/src/GridSplitter/GridSplitter.cs new file mode 100644 index 00000000..b52dfeea --- /dev/null +++ b/components/Sizers/src/GridSplitter/GridSplitter.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Represents the control that redistributes space between columns or rows of a Grid control. +/// +public partial class GridSplitter : SizerBase +{ + private GridResizeDirection _resizeDirection; + private GridResizeBehavior _resizeBehavior; + + /// + /// Gets the target parent grid from level + /// + private FrameworkElement? TargetControl + { + get + { + if (ParentLevel == 0) + { + return this; + } + + // TODO: Can we just use our Visual/Logical Tree extensions for this? + var parent = Parent; + for (int i = 2; i < ParentLevel; i++) // TODO: Why is this 2? We need better documentation on ParentLevel + { + if (parent is FrameworkElement frameworkElement) + { + parent = frameworkElement.Parent; + } + else + { + break; + } + } + + return parent as FrameworkElement; + } + } + + /// + /// Gets GridSplitter Container Grid + /// + private Grid? Resizable => TargetControl?.Parent as Grid; + + /// + /// Gets the current Column definition of the parent Grid + /// + private ColumnDefinition? CurrentColumn + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterTargetedColumnIndex = GetTargetedColumn(); + + if ((gridSplitterTargetedColumnIndex >= 0) + && (gridSplitterTargetedColumnIndex < Resizable.ColumnDefinitions.Count)) + { + return Resizable.ColumnDefinitions[gridSplitterTargetedColumnIndex]; + } + + return null; + } + } + + /// + /// Gets the Sibling Column definition of the parent Grid + /// + private ColumnDefinition? SiblingColumn + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterSiblingColumnIndex = GetSiblingColumn(); + + if ((gridSplitterSiblingColumnIndex >= 0) + && (gridSplitterSiblingColumnIndex < Resizable.ColumnDefinitions.Count)) + { + return Resizable.ColumnDefinitions[gridSplitterSiblingColumnIndex]; + } + + return null; + } + } + + /// + /// Gets the current Row definition of the parent Grid + /// + private RowDefinition? CurrentRow + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterTargetedRowIndex = GetTargetedRow(); + + if ((gridSplitterTargetedRowIndex >= 0) + && (gridSplitterTargetedRowIndex < Resizable.RowDefinitions.Count)) + { + return Resizable.RowDefinitions[gridSplitterTargetedRowIndex]; + } + + return null; + } + } + + /// + /// Gets the Sibling Row definition of the parent Grid + /// + private RowDefinition? SiblingRow + { + get + { + if (Resizable == null) + { + return null; + } + + var gridSplitterSiblingRowIndex = GetSiblingRow(); + + if ((gridSplitterSiblingRowIndex >= 0) + && (gridSplitterSiblingRowIndex < Resizable.RowDefinitions.Count)) + { + return Resizable.RowDefinitions[gridSplitterSiblingRowIndex]; + } + + return null; + } + } +} diff --git a/components/Sizers/src/MultiTarget.props b/components/Sizers/src/MultiTarget.props new file mode 100644 index 00000000..b11c1942 --- /dev/null +++ b/components/Sizers/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/Sizers/src/PropertySizer/PropertySizer.Events.cs b/components/Sizers/src/PropertySizer/PropertySizer.Events.cs new file mode 100644 index 00000000..85d5bf07 --- /dev/null +++ b/components/Sizers/src/PropertySizer/PropertySizer.Events.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +// Events for PropertySizer. +public partial class PropertySizer +{ + private double _currentSize; + + /// + protected override void OnDragStarting() + { + // We grab the current size of the bound value when we start a drag + // and we manipulate from that set point. + if (ReadLocalValue(BindingProperty) != DependencyProperty.UnsetValue) + { + _currentSize = Binding; + } + } + + /// + protected override bool OnDragHorizontal(double horizontalChange) + { + // We use a central function for both horizontal/vertical as + // a general property has no notion of direction when we + // manipulate it, so the logic is abstracted. + return ApplySizeChange(horizontalChange); + } + + /// + protected override bool OnDragVertical(double verticalChange) + { + return ApplySizeChange(verticalChange); + } + + private bool ApplySizeChange(double newSize) + { + newSize = IsDragInverted ? -newSize : newSize; + + // We want to be checking the modified final value for bounds checks. + newSize += _currentSize; + + // Check if we hit the min/max value, as we should use that if we're on the edge + if (ReadLocalValue(MinimumProperty) != DependencyProperty.UnsetValue && + newSize < Minimum) + { + // We use SetValue here as that'll update our bound property vs. overwriting the binding itself. + SetValue(BindingProperty, Minimum); + } + else if (ReadLocalValue(MaximumProperty) != DependencyProperty.UnsetValue && + newSize > Maximum) + { + SetValue(BindingProperty, Maximum); + } + else + { + // Otherwise, we use the value provided. + SetValue(BindingProperty, newSize); + } + + // We're always manipulating the value effectively. + return true; + } +} diff --git a/components/Sizers/src/PropertySizer/PropertySizer.Properties.cs b/components/Sizers/src/PropertySizer/PropertySizer.Properties.cs new file mode 100644 index 00000000..25591d36 --- /dev/null +++ b/components/Sizers/src/PropertySizer/PropertySizer.Properties.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +// Properties for PropertySizer. +public partial class PropertySizer +{ + /// + /// Gets or sets a value indicating whether the control is resizing in the opposite direction. + /// + public bool IsDragInverted + { + get { return (bool)GetValue(IsDragInvertedProperty); } + set { SetValue(IsDragInvertedProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsDragInvertedProperty = + DependencyProperty.Register(nameof(IsDragInverted), typeof(bool), typeof(PropertySizer), new PropertyMetadata(false)); + + /// + /// Gets or sets a two-way binding to a double value that the is manipulating. + /// + /// + /// Note that the binding should be configured to be a TwoWay binding in order for the control to notify the source of the changed value. + /// + /// + /// <controls:PropertySizer Binding="{Binding OpenPaneLength, ElementName=ViewPanel, Mode=TwoWay}"> + /// + public double Binding + { + get { return (double)GetValue(BindingProperty); } + set { SetValue(BindingProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BindingProperty = + DependencyProperty.Register(nameof(Binding), typeof(double), typeof(PropertySizer), new PropertyMetadata(null)); + + /// + /// Gets or sets the minimum allowed value for the to allow for the value. Ignored if not provided. + /// + public double Minimum + { + get { return (double)GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MinimumProperty = + DependencyProperty.Register(nameof(Minimum), typeof(double), typeof(PropertySizer), new PropertyMetadata(0)); + + /// + /// Gets or sets the maximum allowed value for the to allow for the value. Ignored if not provided. + /// + public double Maximum + { + get { return (double)GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty MaximumProperty = + DependencyProperty.Register(nameof(Maximum), typeof(double), typeof(PropertySizer), new PropertyMetadata(0)); +} diff --git a/components/Sizers/src/PropertySizer/PropertySizer.cs b/components/Sizers/src/PropertySizer/PropertySizer.cs new file mode 100644 index 00000000..db6bd140 --- /dev/null +++ b/components/Sizers/src/PropertySizer/PropertySizer.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// The is a control which can be used to manipulate the value of another double based property. For instance manipulating the OpenPaneLength of a NavigationView control. If you are using a , use instead. +/// +public partial class PropertySizer : SizerBase +{ +} diff --git a/components/Sizers/src/SizerAutomationPeer.cs b/components/Sizers/src/SizerAutomationPeer.cs new file mode 100644 index 00000000..16462c7f --- /dev/null +++ b/components/Sizers/src/SizerAutomationPeer.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls.Automation.Peers; + +/// +/// Defines a framework element automation peer for the controls. +/// +public class SizerAutomationPeer : FrameworkElementAutomationPeer +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The that is associated with this . + /// + public SizerAutomationPeer(SizerBase owner) + : base(owner) + { + } + + private SizerBase OwningSizer + { + get + { + return (Owner as SizerBase)!; + } + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + return Owner.GetType().Name; + } + + /// + /// Called by GetName. + /// + /// + /// Returns the first of these that is not null or empty: + /// - Value returned by the base implementation + /// - Name of the owning ContentSizer + /// - ContentSizer class name + /// + protected override string GetNameCore() + { + string name = AutomationProperties.GetName(this.OwningSizer); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + name = this.OwningSizer.Name; + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + name = base.GetNameCore(); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + + return string.Empty; + } +} diff --git a/components/Sizers/src/SizerBase.Events.cs b/components/Sizers/src/SizerBase.Events.cs new file mode 100644 index 00000000..0ded7189 --- /dev/null +++ b/components/Sizers/src/SizerBase.Events.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Event implementations for . +/// +public partial class SizerBase +{ + /// + protected override void OnKeyDown(KeyRoutedEventArgs e) + { + // If we're manipulating with mouse/touch, we ignore keyboard inputs. + if (_dragging) + { + return; + } + + //// TODO: Do we want Ctrl/Shift to be a small increment (kind of inverse to old GridSplitter logic)? + //// var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control); + //// if (ctrl.HasFlag(CoreVirtualKeyStates.Down)) + //// Note: WPF doesn't do anything here. + //// I think if we did anything, we'd create a SmallKeyboardIncrement property? + + // Initialize a drag event for this keyboard interaction. + OnDragStarting(); + + if (Orientation == Orientation.Vertical) + { + var horizontalChange = KeyboardIncrement; + + // Important: adjust for RTL language flow settings and invert horizontal axis +#if !HAS_UNO + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } +#endif + + if (e.Key == Windows.System.VirtualKey.Left) + { + OnDragHorizontal(-horizontalChange); + } + else if (e.Key == Windows.System.VirtualKey.Right) + { + OnDragHorizontal(horizontalChange); + } + } + else + { + if (e.Key == Windows.System.VirtualKey.Up) + { + OnDragVertical(-KeyboardIncrement); + } + else if (e.Key == Windows.System.VirtualKey.Down) + { + OnDragVertical(KeyboardIncrement); + } + } + } + + /// + protected override void OnManipulationStarting(ManipulationStartingRoutedEventArgs e) + { + base.OnManipulationStarting(e); + + OnDragStarting(); + } + + /// + protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) + { + // We use Truncate here to provide 'snapping' points with the DragIncrement property + // It works for both our negative and positive values, as otherwise we'd need to use + // Ceiling when negative and Floor when positive to maintain the correct behavior. + var horizontalChange = + Math.Truncate(e.Cumulative.Translation.X / DragIncrement) * DragIncrement; + var verticalChange = + Math.Truncate(e.Cumulative.Translation.Y / DragIncrement) * DragIncrement; + + // Important: adjust for RTL language flow settings and invert horizontal axis +#if !HAS_UNO + if (this.FlowDirection == FlowDirection.RightToLeft) + { + horizontalChange *= -1; + } +#endif + + if (Orientation == Orientation.Vertical) + { + if (!OnDragHorizontal(horizontalChange)) + { + return; + } + } + else if (Orientation == Orientation.Horizontal) + { + if (!OnDragVertical(verticalChange)) + { + return; + } + } + + base.OnManipulationDelta(e); + } + + // private helper bools for Visual States + private bool _pressed = false; + private bool _dragging = false; + private bool _pointerEntered = false; + + private void SizerBase_PointerReleased(object sender, PointerRoutedEventArgs e) + { + _pressed = false; + + if (IsEnabled) + { + VisualStateManager.GoToState(this, _pointerEntered ? PointerOverState : NormalState, true); + } + } + + private void SizerBase_PointerPressed(object sender, PointerRoutedEventArgs e) + { + _pressed = true; + + if (IsEnabled) + { + VisualStateManager.GoToState(this, PointerOverState, true); + } + } + + private void SizerBase_PointerExited(object sender, PointerRoutedEventArgs e) + { + _pointerEntered = false; + + if (!_pressed && !_dragging && IsEnabled) + { + VisualStateManager.GoToState(this, NormalState, true); + } + } + + private void SizerBase_PointerEntered(object sender, PointerRoutedEventArgs e) + { + _pointerEntered = true; + + if (!_pressed && !_dragging && IsEnabled) + { + VisualStateManager.GoToState(this, PointerOverState, true); + } + } + + private void SizerBase_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) + { + _dragging = false; + _pressed = false; + VisualStateManager.GoToState(this, _pointerEntered ? PointerOverState : NormalState, true); + } + + private void SizerBase_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) + { + _dragging = true; + VisualStateManager.GoToState(this, PressedState, true); + } + + private void SizerBase_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (!IsEnabled) + { + VisualStateManager.GoToState(this, DisabledState, true); + } + else + { + VisualStateManager.GoToState(this, _pointerEntered ? PointerOverState : NormalState, true); + } + } +} diff --git a/components/Sizers/src/SizerBase.Helpers.cs b/components/Sizers/src/SizerBase.Helpers.cs new file mode 100644 index 00000000..e87e09e8 --- /dev/null +++ b/components/Sizers/src/SizerBase.Helpers.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Protected helper methods for and subclasses. +/// +public partial class SizerBase : Control +{ + /// + /// Check for new requested vertical size is valid or not + /// + /// Target control being resized + /// The requested new height + /// The parent control's ActualHeight + /// Bool result if requested vertical change is valid or not + protected static bool IsValidHeight(FrameworkElement target, double newHeight, double parentActualHeight) + { + var minHeight = target.MinHeight; + if (newHeight < 0 || (!double.IsNaN(minHeight) && newHeight < minHeight)) + { + return false; + } + + var maxHeight = target.MaxHeight; + if (!double.IsNaN(maxHeight) && newHeight > maxHeight) + { + return false; + } + + if (newHeight <= parentActualHeight) + { + return false; + } + + return true; + } + + /// + /// Check for new requested horizontal size is valid or not + /// + /// Target control being resized + /// The requested new width + /// The parent control's ActualWidth + /// Bool result if requested horizontal change is valid or not + protected static bool IsValidWidth(FrameworkElement target, double newWidth, double parentActualWidth) + { + var minWidth = target.MinWidth; + if (newWidth < 0 || (!double.IsNaN(minWidth) && newWidth < minWidth)) + { + return false; + } + + var maxWidth = target.MaxWidth; + if (!double.IsNaN(maxWidth) && newWidth > maxWidth) + { + return false; + } + + if (newWidth <= parentActualWidth) + { + return false; + } + + return true; + } +} diff --git a/components/Sizers/src/SizerBase.Properties.cs b/components/Sizers/src/SizerBase.Properties.cs new file mode 100644 index 00000000..3688c72a --- /dev/null +++ b/components/Sizers/src/SizerBase.Properties.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI; + +#if !WINAPPSDK +using CursorEnum = Windows.UI.Core.CoreCursorType; +#else +using Microsoft.UI.Input; +using CursorEnum = Microsoft.UI.Input.InputSystemCursorShape; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Properties for +/// +public partial class SizerBase : Control +{ + /// + /// Gets or sets the cursor to use when hovering over the gripper bar. If left as null, the control will manage the cursor automatically based on the property value (default). + /// + public CursorEnum Cursor + { + get { return (CursorEnum)GetValue(CursorProperty); } + set { SetValue(CursorProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty CursorProperty = + DependencyProperty.Register(nameof(Cursor), typeof(CursorEnum), typeof(SizerBase), new PropertyMetadata(null, OnOrientationPropertyChanged)); + + /// + /// Gets or sets the incremental amount of change for dragging with the mouse or touch of a sizer control. Effectively a snapping increment for changes. The default is 1. + /// + /// + /// For instance, if the DragIncrement is set to 16. Then when a component is resized with the sizer, it will only increase or decrease in size in that increment. I.e. -16, 0, 16, 32, 48, etc... + /// + /// + /// This value is independent of the property. If you need to provide consistent snapping when moving regardless of input device, set these properties to the same value. + /// + public double DragIncrement + { + get { return (double)GetValue(DragIncrementProperty); } + set { SetValue(DragIncrementProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DragIncrementProperty = + DependencyProperty.Register(nameof(DragIncrement), typeof(double), typeof(SizerBase), new PropertyMetadata(1d)); + + /// + /// Gets or sets the distance each press of an arrow key moves a sizer control. The default is 8. + /// + /// + /// This value is independent of the setting when using mouse/touch. If you want a consistent behavior regardless of input device, set them to the same value if snapping is required. + /// + public double KeyboardIncrement + { + get { return (double)GetValue(KeyboardIncrementProperty); } + set { SetValue(KeyboardIncrementProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty KeyboardIncrementProperty = + DependencyProperty.Register(nameof(KeyboardIncrement), typeof(double), typeof(SizerBase), new PropertyMetadata(8d)); + + /// + /// Gets or sets the orientation the sizer will be and how it will interact with other elements. Defaults to . + /// + /// + /// Note if using , use the property instead. + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(SizerBase), new PropertyMetadata(Orientation.Vertical, OnOrientationPropertyChanged)); + + /// + /// Gets or sets if the Thumb is visible. If not visible, only the background and cursor will be shown on MouseOver or Pressed states. + /// + public bool IsThumbVisible + { + get { return (bool)GetValue(IsThumbVisibleProperty); } + set { SetValue(IsThumbVisibleProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsThumbVisibleProperty = + DependencyProperty.Register(nameof(IsThumbVisible), typeof(bool), typeof(SizerBase), new PropertyMetadata(true, OnIsThumbVisiblePropertyChanged)); + + + private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is SizerBase gripper) + { + VisualStateManager.GoToState(gripper, gripper.Orientation == Orientation.Vertical ? VerticalState : HorizontalState, true); + + CursorEnum cursorByOrientation = gripper.Orientation == Orientation.Vertical ? CursorEnum.SizeWestEast : CursorEnum.SizeNorthSouth; + + // See if there's been a cursor override, otherwise we'll pick + var cursor = gripper.ReadLocalValue(CursorProperty); + if (cursor == DependencyProperty.UnsetValue || cursor == null) + { + cursor = cursorByOrientation; + } + +#if !WINAPPSDK + // On UWP, we use our XAML extension to control this behavior, + // so we'll update it here (and maintain any cursor override). + if (cursor is CursorEnum cursorValue) + { + FrameworkElementExtensions.SetCursor(gripper, cursorValue); + } + + return; +#endif + + // TODO: [UNO] Only supported on certain platforms + // See ProtectedCursor here: https://github.com/unoplatform/uno/blob/3fe3862b270b99dbec4d830b547942af61b1a1d9/src/Uno.UI/UI/Xaml/UIElement.cs#L1015-L1023 +#if WINAPPSDK && !HAS_UNO + // Need to wait until we're at least applying template step of loading before setting Cursor + // See https://github.com/microsoft/microsoft-ui-xaml/issues/7062 + if (gripper._appliedTemplate && + cursor is CursorEnum cursorValue && + (gripper.ProtectedCursor == null || + (gripper.ProtectedCursor is InputSystemCursor current && + current.CursorShape != cursorValue))) + { + gripper.ProtectedCursor = InputSystemCursor.Create(cursorValue); + } +#endif + } + } + private static void OnIsThumbVisiblePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is SizerBase gripper) + { + VisualStateManager.GoToState(gripper, gripper.IsThumbVisible ? VisibleState : CollapsedState, true); + } + } +} diff --git a/components/Sizers/src/SizerBase.cs b/components/Sizers/src/SizerBase.cs new file mode 100644 index 00000000..4c925054 --- /dev/null +++ b/components/Sizers/src/SizerBase.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls.Automation.Peers; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Base class for splitting/resizing type controls like and . Acts similar to an enlarged type control, but with keyboard support. Subclasses should override the various abstract methods here to implement their behavior. +/// + +[TemplateVisualState(Name = NormalState, GroupName = CommonStates)] +[TemplateVisualState(Name = PointerOverState, GroupName = CommonStates)] +[TemplateVisualState(Name = PressedState, GroupName = CommonStates)] +[TemplateVisualState(Name = DisabledState, GroupName = CommonStates)] +[TemplateVisualState(Name = HorizontalState, GroupName = OrientationStates)] +[TemplateVisualState(Name = VerticalState, GroupName = OrientationStates)] +[TemplateVisualState(Name = VisibleState, GroupName = ThumbVisibilityStates)] +[TemplateVisualState(Name = CollapsedState, GroupName = ThumbVisibilityStates)] +public abstract partial class SizerBase : Control +{ + internal const string CommonStates = "CommonStates"; + internal const string NormalState = "Normal"; + internal const string PointerOverState = "PointerOver"; + internal const string PressedState = "Pressed"; + internal const string DisabledState = "Disabled"; + internal const string OrientationStates = "OrientationStates"; + internal const string HorizontalState = "Horizontal"; + internal const string VerticalState = "Vertical"; + internal const string ThumbVisibilityStates = "ThumbVisibilityStates"; + internal const string VisibleState = "Visible"; + internal const string CollapsedState = "Collapsed"; + /// + /// Called when the control has been initialized. + /// + /// Loaded event args. + protected virtual void OnLoaded(RoutedEventArgs e) + { + } + + /// + /// Called when the control starts to be dragged by the user. + /// Implementer should record current state of manipulated target at this point in time. + /// They will receive the cumulative change in or + /// based on the property. + /// + /// + /// This method is also called at the start of a keyboard interaction. Keyboard strokes use the same pattern to emulate a mouse movement for a single change. The appropriate + /// or + /// method will also be called after when the keyboard is used. + /// + protected abstract void OnDragStarting(); + + /// + /// Method to process the requested horizontal resize. + /// + /// The horizontal change amount from the start in device-independent pixels DIP. + /// indicates if a change was made + /// + /// The value provided here is the cumulative change from the beginning of the + /// manipulation. This method will be used regardless of input device. It will already + /// be adjusted for RightToLeft of the containing + /// layout/settings. It will also already account for any settings such as + /// or . The implementer + /// just needs to use the provided value to manipulate their baseline stored + /// in to provide the desired change. + /// + protected abstract bool OnDragHorizontal(double horizontalChange); + + /// + /// Method to process the requested vertical resize. + /// + /// The vertical change amount from the start in device-independent pixels DIP. + /// indicates if a change was made + /// + /// The value provided here is the cumulative change from the beginning of the + /// manipulation. This method will be used regardless of input device. It will also + /// already account for any settings such as or + /// . The implementer just needs + /// to use the provided value to manipulate their baseline stored + /// in to provide the desired change. + /// + protected abstract bool OnDragVertical(double verticalChange); + + /// + /// Initializes a new instance of the class. + /// + public SizerBase() + { + this.DefaultStyleKey = typeof(SizerBase); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new SizerAutomationPeer(this); + } + +// On Uno the ProtectedCursor isn't supported yet, so we don't need this value. +#if WINAPPSDK && !HAS_UNO + // Used to track when we're in the OnApplyTemplateStep to change ProtectedCursor value. + private bool _appliedTemplate = false; +#endif + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Unregister Events + Loaded -= SizerBase_Loaded; + PointerEntered -= SizerBase_PointerEntered; + PointerExited -= SizerBase_PointerExited; + PointerPressed -= SizerBase_PointerPressed; + PointerReleased -= SizerBase_PointerReleased; + ManipulationStarted -= SizerBase_ManipulationStarted; + ManipulationCompleted -= SizerBase_ManipulationCompleted; + IsEnabledChanged -= SizerBase_IsEnabledChanged; + + // Register Events + Loaded += SizerBase_Loaded; + PointerEntered += SizerBase_PointerEntered; + PointerExited += SizerBase_PointerExited; + PointerPressed += SizerBase_PointerPressed; + PointerReleased += SizerBase_PointerReleased; + ManipulationStarted += SizerBase_ManipulationStarted; + ManipulationCompleted += SizerBase_ManipulationCompleted; + IsEnabledChanged += SizerBase_IsEnabledChanged; + + // Trigger initial state transition based on if we're Enabled or not currently. + SizerBase_IsEnabledChanged(this, null!); +#if WINAPPSDK && !HAS_UNO + // On WinAppSDK, we'll trigger this to setup the initial ProtectedCursor value. + _appliedTemplate = true; +#endif + // Ensure we have the proper cursor value setup, as we can only set now for WinUI 3 + OnOrientationPropertyChanged(this, null!); + + // Ensure we set the Thumb visiblity + OnIsThumbVisiblePropertyChanged(this, null!); + } + + private void SizerBase_Loaded(object sender, RoutedEventArgs e) + { + Loaded -= SizerBase_Loaded; + + OnLoaded(e); + } +} diff --git a/components/Sizers/src/SizerBase.xaml b/components/Sizers/src/SizerBase.xaml new file mode 100644 index 00000000..7983bb52 --- /dev/null +++ b/components/Sizers/src/SizerBase.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + 4 + 2 + 4 + + + + diff --git a/components/Sizers/src/Strings/en-US/Resources.resw b/components/Sizers/src/Strings/en-US/Resources.resw new file mode 100644 index 00000000..3e0ca58b --- /dev/null +++ b/components/Sizers/src/Strings/en-US/Resources.resw @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Sizer + Narrator Resource for SizerBase controls and similar + + \ No newline at end of file diff --git a/components/Sizers/src/Themes/Generic.xaml b/components/Sizers/src/Themes/Generic.xaml new file mode 100644 index 00000000..87d3e588 --- /dev/null +++ b/components/Sizers/src/Themes/Generic.xaml @@ -0,0 +1,7 @@ + + + + + + diff --git a/components/Sizers/tests/ExampleSizerBaseTestClass.cs b/components/Sizers/tests/ExampleSizerBaseTestClass.cs new file mode 100644 index 00000000..4d0385e7 --- /dev/null +++ b/components/Sizers/tests/ExampleSizerBaseTestClass.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls; +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.WinUI.Controls.Automation.Peers; + +namespace SizersExperiment.Tests; + +[TestClass] +public partial class ExampleSizerBaseTestClass : VisualUITestBase +{ + [TestMethod] + public async Task ShouldConfigureGridSplitterAutomationPeer() + { + await EnqueueAsync(() => + { + const string automationName = "MyCustomAutomationName"; + const string name = "Sizer"; + + var gridSplitter = new GridSplitter(); + var gridSplitterAutomationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(gridSplitter) as SizerAutomationPeer; + + Assert.IsNotNull(gridSplitterAutomationPeer, "Verify that the AutomationPeer is SizerAutomationPeer."); + + gridSplitter.Name = name; + Assert.IsTrue(gridSplitterAutomationPeer.GetName().Contains(name), "Verify that the UIA name contains the given Name of the GridSplitter (Sizer)."); + + gridSplitter.SetValue(AutomationProperties.NameProperty, automationName); + Assert.IsTrue(gridSplitterAutomationPeer.GetName().Contains(automationName), "Verify that the UIA name contains the customized AutomationProperties.Name of the GridSplitter."); + }); + } + + [UIThreadTestMethod] + public void PropertySizer_TestInitialBinding(PropertySizerTestInitialBinding testControl) + { + var propertySizer = testControl.FindDescendant(); + + Assert.IsNotNull(propertySizer, "Could not find PropertySizer control."); + + // Set in XAML Page LINK: PropertySizerTestInitialBinding.xaml#L14 + Assert.AreEqual(300, propertySizer.Binding, "Property Sizer not at expected initial value."); + } + + [UIThreadTestMethod] + public void PropertySizer_TestChangeBinding(PropertySizerTestInitialBinding testControl) + { + var propertySizer = testControl.FindDescendant(); + var navigationView = testControl.FindDescendant(); + + Assert.IsNotNull(propertySizer, "Could not find PropertySizer control."); + Assert.IsNotNull(navigationView, "Could not find NavigationView control."); + + navigationView.OpenPaneLength = 200; + + // Set in XAML Page LINK: PropertySizerTestInitialBinding.xaml#L14 + Assert.AreEqual(200, propertySizer.Binding, "Property Sizer not at expected changed value."); + } +} diff --git a/components/Sizers/tests/PropertySizerTestInitialBinding.xaml b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml new file mode 100644 index 00000000..ef3ab0da --- /dev/null +++ b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/components/Sizers/tests/PropertySizerTestInitialBinding.xaml.cs b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml.cs new file mode 100644 index 00000000..f495c475 --- /dev/null +++ b/components/Sizers/tests/PropertySizerTestInitialBinding.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SizersExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class PropertySizerTestInitialBinding : Page +{ + public PropertySizerTestInitialBinding() + { + this.InitializeComponent(); + } +} diff --git a/components/Sizers/tests/Sizers.Tests.projitems b/components/Sizers/tests/Sizers.Tests.projitems new file mode 100644 index 00000000..0773d66f --- /dev/null +++ b/components/Sizers/tests/Sizers.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + FE1BF6B1-E2B8-4F75-9629-97B5AA077FAA + + + SizersExperiment.Tests + + + + + PropertySizerTestInitialBinding.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Sizers/tests/Sizers.Tests.shproj b/components/Sizers/tests/Sizers.Tests.shproj new file mode 100644 index 00000000..ef8fa866 --- /dev/null +++ b/components/Sizers/tests/Sizers.Tests.shproj @@ -0,0 +1,13 @@ + + + + FE1BF6B1-E2B8-4F75-9629-97B5AA077FAA + 14.0 + + + + + + + +