diff --git a/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj b/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj
index b2e21b3ea..c7f038663 100644
--- a/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj
+++ b/labs/SizerBase/src/CommunityToolkit.Labs.WinUI.SizerBase.csproj
@@ -12,4 +12,8 @@
0.0.1
+
+
+
+
diff --git a/labs/SizerBase/src/ContentSizer/ContentSizer.Events.cs b/labs/SizerBase/src/ContentSizer/ContentSizer.Events.cs
new file mode 100644
index 000000000..00b7ec796
--- /dev/null
+++ b/labs/SizerBase/src/ContentSizer/ContentSizer.Events.cs
@@ -0,0 +1,82 @@
+// 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.UI;
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+// 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/labs/SizerBase/src/ContentSizer/ContentSizer.Properties.cs b/labs/SizerBase/src/ContentSizer/ContentSizer.Properties.cs
new file mode 100644
index 000000000..d24d88ddb
--- /dev/null
+++ b/labs/SizerBase/src/ContentSizer/ContentSizer.Properties.cs
@@ -0,0 +1,71 @@
+// 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.
+
+#if !WINAPPSDK
+using System;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+// 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/labs/SizerBase/src/ContentSizer/ContentSizer.cs b/labs/SizerBase/src/ContentSizer/ContentSizer.cs
new file mode 100644
index 000000000..a69481856
--- /dev/null
+++ b/labs/SizerBase/src/ContentSizer/ContentSizer.cs
@@ -0,0 +1,18 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/SizerBase/src/Dependencies/DependencyObjectExtensions.cs b/labs/SizerBase/src/Dependencies/DependencyObjectExtensions.cs
new file mode 100644
index 000000000..33d62b07e
--- /dev/null
+++ b/labs/SizerBase/src/Dependencies/DependencyObjectExtensions.cs
@@ -0,0 +1,235 @@
+// 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;
+using System.Collections.Generic;
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+#endif
+
+namespace CommunityToolkit.WinUI.UI;
+
+//// IMPORTANT NOTE: This is the old 6.1.1 version of the extensions as I had issues with TPredicate with the new ones here for some reason and just wanted to get this working for now.
+
+///
+/// Defines a collection of extensions methods for UI.
+///
+public static class VisualTree
+{
+ ///
+ /// Find descendant control using its name.
+ ///
+ /// Parent element.
+ /// Name of the control to find
+ /// Descendant control or null if not found.
+ public static FrameworkElement? FindDescendantByName(this DependencyObject element, string name)
+ {
+ if (element == null || string.IsNullOrWhiteSpace(name))
+ {
+ return null;
+ }
+
+ if (name.Equals((element as FrameworkElement)?.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return element as FrameworkElement;
+ }
+
+ var childCount = VisualTreeHelper.GetChildrenCount(element);
+ for (int i = 0; i < childCount; i++)
+ {
+ var result = VisualTreeHelper.GetChild(element, i).FindDescendantByName(name);
+ if (result != null)
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Find first descendant control of a specified type.
+ ///
+ /// Type to search for.
+ /// Parent element.
+ /// Descendant control or null if not found.
+ public static T? FindDescendant(this DependencyObject element)
+ where T : DependencyObject
+ {
+ T? retValue = default(T);
+ var childrenCount = VisualTreeHelper.GetChildrenCount(element);
+
+ for (var i = 0; i < childrenCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(element, i);
+ if (child is T type)
+ {
+ retValue = type;
+ break;
+ }
+
+ retValue = FindDescendant(child);
+
+ if (retValue != null)
+ {
+ break;
+ }
+ }
+
+ return retValue;
+ }
+
+ ///
+ /// Find first descendant control of a specified type.
+ ///
+ /// Parent element.
+ /// Type of descendant.
+ /// Descendant control or null if not found.
+ public static object? FindDescendant(this DependencyObject element, Type type)
+ {
+ object? retValue = null;
+ var childrenCount = VisualTreeHelper.GetChildrenCount(element);
+
+ for (var i = 0; i < childrenCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(element, i);
+ if (child.GetType() == type)
+ {
+ retValue = child;
+ break;
+ }
+
+ retValue = FindDescendant(child, type);
+
+ if (retValue != null)
+ {
+ break;
+ }
+ }
+
+ return retValue;
+ }
+
+ ///
+ /// Find all descendant controls of the specified type.
+ ///
+ /// Type to search for.
+ /// Parent element.
+ /// Descendant controls or empty if not found.
+ public static IEnumerable FindDescendants(this DependencyObject element)
+ where T : DependencyObject
+ {
+ var childrenCount = VisualTreeHelper.GetChildrenCount(element);
+
+ for (var i = 0; i < childrenCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(element, i);
+ if (child is T type)
+ {
+ yield return type;
+ }
+
+ foreach (T childofChild in child.FindDescendants())
+ {
+ yield return childofChild;
+ }
+ }
+ }
+
+ ///
+ /// Find visual ascendant control using its name.
+ ///
+ /// Parent element.
+ /// Name of the control to find
+ /// Descendant control or null if not found.
+ public static FrameworkElement? FindAscendantByName(this DependencyObject element, string name)
+ {
+ if (element == null || string.IsNullOrWhiteSpace(name))
+ {
+ return null;
+ }
+
+ var parent = VisualTreeHelper.GetParent(element);
+
+ if (parent == null)
+ {
+ return null;
+ }
+
+ if (name.Equals((parent as FrameworkElement)?.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return parent as FrameworkElement;
+ }
+
+ return parent.FindAscendantByName(name);
+ }
+
+ ///
+ /// Find first visual ascendant control of a specified type.
+ ///
+ /// Type to search for.
+ /// Child element.
+ /// Ascendant control or null if not found.
+ public static T? FindAscendant(this DependencyObject element)
+ where T : DependencyObject
+ {
+ var parent = VisualTreeHelper.GetParent(element);
+
+ if (parent == null)
+ {
+ return default(T);
+ }
+
+ if (parent is T rtn)
+ {
+ return rtn;
+ }
+
+ return parent.FindAscendant();
+ }
+
+ ///
+ /// Find first visual ascendant control of a specified type.
+ ///
+ /// Child element.
+ /// Type of ascendant to look for.
+ /// Ascendant control or null if not found.
+ public static object? FindAscendant(this DependencyObject element, Type type)
+ {
+ var parent = VisualTreeHelper.GetParent(element);
+
+ if (parent == null)
+ {
+ return null;
+ }
+
+ if (parent.GetType() == type)
+ {
+ return parent;
+ }
+
+ return parent.FindAscendant(type);
+ }
+
+ ///
+ /// Find all visual ascendants for the element.
+ ///
+ /// Child element.
+ /// A collection of parent elements or null if none found.
+ public static IEnumerable FindAscendants(this DependencyObject element)
+ {
+ var parent = VisualTreeHelper.GetParent(element);
+
+ while (parent != null)
+ {
+ yield return parent;
+ parent = VisualTreeHelper.GetParent(parent);
+ }
+ }
+}
diff --git a/labs/SizerBase/src/GridSplitter/GridSplitter.Data.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Data.cs
new file mode 100644
index 000000000..65c48ca80
--- /dev/null
+++ b/labs/SizerBase/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.Labs.WinUI;
+
+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/labs/SizerBase/src/GridSplitter/GridSplitter.Events.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Events.cs
new file mode 100644
index 000000000..0de655a45
--- /dev/null
+++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Events.cs
@@ -0,0 +1,177 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+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/labs/SizerBase/src/GridSplitter/GridSplitter.Helpers.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Helpers.cs
new file mode 100644
index 000000000..6af39fc53
--- /dev/null
+++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Helpers.cs
@@ -0,0 +1,253 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+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/labs/SizerBase/src/GridSplitter/GridSplitter.Properties.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.Properties.cs
new file mode 100644
index 000000000..1e6c47ab0
--- /dev/null
+++ b/labs/SizerBase/src/GridSplitter/GridSplitter.Properties.cs
@@ -0,0 +1,86 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+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/labs/SizerBase/src/GridSplitter/GridSplitter.cs b/labs/SizerBase/src/GridSplitter/GridSplitter.cs
new file mode 100644
index 000000000..c7aa17b16
--- /dev/null
+++ b/labs/SizerBase/src/GridSplitter/GridSplitter.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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/SizerBase/src/PropertySizer/PropertySizer.Events.cs b/labs/SizerBase/src/PropertySizer/PropertySizer.Events.cs
new file mode 100644
index 000000000..c073ce851
--- /dev/null
+++ b/labs/SizerBase/src/PropertySizer/PropertySizer.Events.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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+#else
+using Microsoft.UI.Xaml;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+// 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/labs/SizerBase/src/PropertySizer/PropertySizer.Properties.cs b/labs/SizerBase/src/PropertySizer/PropertySizer.Properties.cs
new file mode 100644
index 000000000..1d2959f33
--- /dev/null
+++ b/labs/SizerBase/src/PropertySizer/PropertySizer.Properties.cs
@@ -0,0 +1,81 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+#else
+using Microsoft.UI.Xaml;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+// 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/labs/SizerBase/src/PropertySizer/PropertySizer.cs b/labs/SizerBase/src/PropertySizer/PropertySizer.cs
new file mode 100644
index 000000000..aa4377c5f
--- /dev/null
+++ b/labs/SizerBase/src/PropertySizer/PropertySizer.cs
@@ -0,0 +1,18 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/SizerBase/src/SizerAutomationPeer.cs b/labs/SizerBase/src/SizerAutomationPeer.cs
new file mode 100644
index 000000000..5d59103d1
--- /dev/null
+++ b/labs/SizerBase/src/SizerAutomationPeer.cs
@@ -0,0 +1,83 @@
+// 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.Labs.WinUI;
+
+#if !WINAPPSDK
+using Windows.UI.Xaml.Automation;
+using Windows.UI.Xaml.Automation.Peers;
+#else
+using Microsoft.UI.Xaml.Automation;
+using Microsoft.UI.Xaml.Automation.Peers;
+#endif
+
+
+namespace CommunityToolkit.Labs.WinUI.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/labs/SizerBase/src/SizerBase.Events.cs b/labs/SizerBase/src/SizerBase.Events.cs
new file mode 100644
index 000000000..38d3f2893
--- /dev/null
+++ b/labs/SizerBase/src/SizerBase.Events.cs
@@ -0,0 +1,190 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Input;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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 (this.FlowDirection == FlowDirection.RightToLeft)
+ {
+ horizontalChange *= -1;
+ }
+
+ 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 Trancate 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 (this.FlowDirection == FlowDirection.RightToLeft)
+ {
+ horizontalChange *= -1;
+ }
+
+ 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 ? "PointerOver" : "Normal", true);
+ }
+ }
+
+ private void SizerBase_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ _pressed = true;
+
+ if (IsEnabled)
+ {
+ VisualStateManager.GoToState(this, "Pressed", true);
+ }
+ }
+
+ private void SizerBase_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ _pointerEntered = false;
+
+ if (!_pressed && !_dragging && IsEnabled)
+ {
+ VisualStateManager.GoToState(this, "Normal", true);
+ }
+ }
+
+ private void SizerBase_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ _pointerEntered = true;
+
+ if (!_pressed && !_dragging && IsEnabled)
+ {
+ VisualStateManager.GoToState(this, "PointerOver", true);
+ }
+ }
+
+ private void SizerBase_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
+ {
+ _dragging = false;
+ _pressed = false;
+ VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true);
+ }
+
+ private void SizerBase_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
+ {
+ _dragging = true;
+ VisualStateManager.GoToState(this, "Pressed", true);
+ }
+
+ private void SizerBase_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (!IsEnabled)
+ {
+ VisualStateManager.GoToState(this, "Disabled", true);
+ }
+ else
+ {
+ VisualStateManager.GoToState(this, _pointerEntered ? "PointerOver" : "Normal", true);
+ }
+ }
+}
diff --git a/labs/SizerBase/src/SizerBase.Helpers.cs b/labs/SizerBase/src/SizerBase.Helpers.cs
new file mode 100644
index 000000000..3fa67feec
--- /dev/null
+++ b/labs/SizerBase/src/SizerBase.Helpers.cs
@@ -0,0 +1,77 @@
+// 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.
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// 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/labs/SizerBase/src/SizerBase.Properties.cs b/labs/SizerBase/src/SizerBase.Properties.cs
new file mode 100644
index 000000000..aa7011ab9
--- /dev/null
+++ b/labs/SizerBase/src/SizerBase.Properties.cs
@@ -0,0 +1,102 @@
+// 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 Windows.UI.Core;
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
+
+///
+/// Properties for
+///
+public partial class SizerBase : Control
+{
+ ///
+ /// Gets or sets the cursor to use when hovering over the gripper bar.
+ ///
+ public CoreCursorType Cursor
+ {
+ get { return (CoreCursorType)GetValue(CursorProperty); }
+ set { SetValue(CursorProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty CursorProperty =
+ DependencyProperty.Register(nameof(Cursor), typeof(CoreCursorType), typeof(SizerBase), new PropertyMetadata(CoreCursorType.SizeWestEast));
+
+ ///
+ /// Gets or sets the incremental amount of change for draging 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 indepedent 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));
+
+ private static void OnOrientationPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is SizerBase gripper)
+ {
+ // TODO: For WinUI 3, we will just be setting the ProtectedCursor property directly.
+ gripper.Cursor = gripper.Orientation == Orientation.Vertical ? CoreCursorType.SizeWestEast : CoreCursorType.SizeNorthSouth;
+ }
+ }
+}
diff --git a/labs/SizerBase/src/SizerBase.cs b/labs/SizerBase/src/SizerBase.cs
index c6ddf05bd..690220209 100644
--- a/labs/SizerBase/src/SizerBase.cs
+++ b/labs/SizerBase/src/SizerBase.cs
@@ -2,14 +2,129 @@
// 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;
-using System.Collections.Generic;
+using CommunityToolkit.Labs.WinUI.Automation.Peers;
+
+#if !WINAPPSDK
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Automation.Peers;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Input;
+#else
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
-using Windows.Foundation;
+using Microsoft.UI.Xaml.Input;
+#endif
+
+namespace CommunityToolkit.Labs.WinUI;
-namespace CommunityToolkit.Labs.WinUI
+///
+/// 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.
+///
+public abstract partial class SizerBase : Control
{
- public class SizerBase
+ ///
+ /// 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.
+ /// Implementor 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 implementor
+ /// 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 implementor 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);
+ }
+
+ ///
+ 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!);
+ }
+
+ private void SizerBase_Loaded(object sender, RoutedEventArgs e)
+ {
+ Loaded -= SizerBase_Loaded;
+
+ OnLoaded(e);
}
}
diff --git a/labs/SizerBase/src/SizerBase.xaml b/labs/SizerBase/src/SizerBase.xaml
new file mode 100644
index 000000000..cb172b317
--- /dev/null
+++ b/labs/SizerBase/src/SizerBase.xaml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/labs/SizerBase/src/Strings/en-US/Resources.resw b/labs/SizerBase/src/Strings/en-US/Resources.resw
new file mode 100644
index 000000000..d9201f3ce
--- /dev/null
+++ b/labs/SizerBase/src/Strings/en-US/Resources.resw
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ Collapse Blade
+ Narrator Resource for BladeView collapsed status
+
+
+ Expand Blade
+ Narrator Resource for BladeView expanded status
+
+
+ Sizer
+ Narrator Resource for SizerBase controls and similar
+
+
\ No newline at end of file
diff --git a/labs/SizerBase/src/Themes/Generic.xaml b/labs/SizerBase/src/Themes/Generic.xaml
new file mode 100644
index 000000000..82fb0ba4b
--- /dev/null
+++ b/labs/SizerBase/src/Themes/Generic.xaml
@@ -0,0 +1,6 @@
+
+
+
+
+