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
+
+
+
+
+
+
+
+