diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml b/components/Segmented/samples/SegmentedBasicSample.xaml index f00f791e..f3307afa 100644 --- a/components/Segmented/samples/SegmentedBasicSample.xaml +++ b/components/Segmented/samples/SegmentedBasicSample.xaml @@ -14,6 +14,7 @@ [ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")] [ToolkitSampleMultiChoiceOption("Alignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal alignment")] +[ToolkitSampleMultiChoiceOption("OrientationMode", "Horizontal", "Vertical", Title = "Orientation")] [ToolkitSample(id: nameof(SegmentedBasicSample), "Basics", description: $"A sample for showing how to create and use a {nameof(Segmented)} custom control.")] public sealed partial class SegmentedBasicSample : Page @@ -36,5 +37,12 @@ public SegmentedBasicSample() "Stretch" => HorizontalAlignment.Stretch, _ => throw new System.NotImplementedException(), }; + + public static Orientation ConvertStringToOrientation(string orientation) => orientation switch + { + "Horizontal" => Orientation.Horizontal, + "Vertical" => Orientation.Vertical, + _ => throw new System.NotImplementedException(), + }; } diff --git a/components/Segmented/src/EqualPanel.cs b/components/Segmented/src/EqualPanel.cs index 3caa7cbf..88b4ba11 100644 --- a/components/Segmented/src/EqualPanel.cs +++ b/components/Segmented/src/EqualPanel.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.UI.Xaml.Controls; using System.Data; namespace CommunityToolkit.WinUI.Controls; @@ -14,15 +15,6 @@ public partial class EqualPanel : Panel private double _maxItemWidth = 0; private double _maxItemHeight = 0; private int _visibleItemsCount = 0; - - /// - /// Gets or sets the spacing between items. - /// - public double Spacing - { - get { return (double)GetValue(SpacingProperty); } - set { SetValue(SpacingProperty, value); } - } /// /// Identifies the Spacing dependency property. @@ -32,14 +24,41 @@ public double Spacing nameof(Spacing), typeof(double), typeof(EqualPanel), - new PropertyMetadata(default(double), OnSpacingChanged)); + new PropertyMetadata(default(double), OnPropertyChanged)); + + /// + /// Backing for the property. + /// + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(EqualPanel), + new PropertyMetadata(default(Orientation), OnPropertyChanged)); + + /// + /// Gets or sets the spacing between items. + /// + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + /// + /// Gets or sets the panel orientation. + /// + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } /// /// Creates a new instance of the class. /// public EqualPanel() { - RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged); + RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnAlignmentChanged); } /// @@ -60,19 +79,39 @@ protected override Size MeasureOverride(Size availableSize) if (_visibleItemsCount > 0) { - // Return equal widths based on the widest item - // In very specific edge cases the AvailableWidth might be infinite resulting in a crash. - if (HorizontalAlignment != HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width)) + bool stretch = Orientation switch + { + Orientation.Horizontal => HorizontalAlignment is HorizontalAlignment.Stretch && !double.IsInfinity(availableSize.Width), + Orientation.Vertical or _ => VerticalAlignment is VerticalAlignment.Stretch && !double.IsInfinity(availableSize.Height), + }; + + // Define XY coords + double xSize = 0, ySize = 0; + + // Define UV coords for orientation agnostic XY manipulation + ref double uSize = ref SelectAxis(Orientation, ref xSize, ref ySize, true); + ref double vSize = ref SelectAxis(Orientation, ref xSize, ref ySize, false); + ref double maxItemU = ref SelectAxis(Orientation, ref _maxItemWidth, ref _maxItemHeight, true); + ref double maxItemV = ref SelectAxis(Orientation, ref _maxItemWidth, ref _maxItemHeight, false); + double availableU = Orientation is Orientation.Horizontal ? availableSize.Width : availableSize.Height; + + if (stretch) { - return new Size((_maxItemWidth * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)), _maxItemHeight); + // Adjust maxItemU to form equal rows/columns by available U space (adjust for spacing) + double totalU = availableU - (Spacing * (_visibleItemsCount - 1)); + maxItemU = totalU / _visibleItemsCount; + + // Set uSize/vSize for XY result construction + uSize = availableU; + vSize = maxItemV; } else { - // Equal columns based on the available width, adjust for spacing - double totalWidth = availableSize.Width - (Spacing * (_visibleItemsCount - 1)); - _maxItemWidth = totalWidth / _visibleItemsCount; - return new Size(availableSize.Width, _maxItemHeight); + uSize = (maxItemU * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)); + vSize = maxItemV; } + + return new Size(xSize, ySize); } else { @@ -83,31 +122,53 @@ protected override Size MeasureOverride(Size availableSize) /// protected override Size ArrangeOverride(Size finalSize) { + // Define X and Y double x = 0; + double y = 0; + // Define UV axis + ref double u = ref x; + ref double maxItemU = ref _maxItemWidth; + double finalSizeU = finalSize.Width; + if (Orientation is Orientation.Vertical) + { + u = ref y; + maxItemU = ref _maxItemHeight; + finalSizeU = finalSize.Height; + } + // Check if there's more (little) width available - if so, set max item width to the maximum possible as we have an almost perfect height. - if (finalSize.Width > _visibleItemsCount * _maxItemWidth + (Spacing * (_visibleItemsCount - 1))) + if (finalSizeU > _visibleItemsCount * maxItemU + (Spacing * (_visibleItemsCount - 1))) { - _maxItemWidth = (finalSize.Width - (Spacing * (_visibleItemsCount - 1))) / _visibleItemsCount; + maxItemU = (finalSizeU - (Spacing * (_visibleItemsCount - 1))) / _visibleItemsCount; } var elements = Children.Where(static e => e.Visibility == Visibility.Visible); foreach (var child in elements) { - child.Arrange(new Rect(x, 0, _maxItemWidth, _maxItemHeight)); - x += _maxItemWidth + Spacing; + // NOTE: The arrange method is still in X/Y coordinate system + child.Arrange(new Rect(x, y, _maxItemWidth, _maxItemHeight)); + u += maxItemU + Spacing; } return finalSize; } - private void OnHorizontalAlignmentChanged(DependencyObject sender, DependencyProperty dp) + private void OnAlignmentChanged(DependencyObject sender, DependencyProperty dp) { InvalidateMeasure(); } - private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var panel = (EqualPanel)d; panel.InvalidateMeasure(); } + + private static ref double SelectAxis(Orientation orientation, ref double x, ref double y, bool u) + { + if ((orientation is Orientation.Horizontal && u) || (orientation is Orientation.Vertical && !u)) + return ref x; + else + return ref y; + } } diff --git a/components/Segmented/src/Segmented/Segmented.Properties.cs b/components/Segmented/src/Segmented/Segmented.Properties.cs new file mode 100644 index 00000000..c28d7a4e --- /dev/null +++ b/components/Segmented/src/Segmented/Segmented.Properties.cs @@ -0,0 +1,26 @@ +// 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 Segmented +{ + /// + /// The backing for the property. + /// + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(Segmented), + new PropertyMetadata(Orientation.Horizontal, (d, e) => ((Segmented)d).OnOrientationChanged())); + + /// + /// Gets or sets the orientation. + /// + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } +} diff --git a/components/Segmented/src/Segmented/Segmented.cs b/components/Segmented/src/Segmented/Segmented.cs index d649ce46..938dca8e 100644 --- a/components/Segmented/src/Segmented/Segmented.cs +++ b/components/Segmented/src/Segmented/Segmented.cs @@ -22,6 +22,7 @@ public Segmented() this.DefaultStyleKey = typeof(Segmented); RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged); + RegisterPropertyChangedCallback(OrientationProperty, OnSelectedIndexChanged); } /// @@ -154,4 +155,15 @@ private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty _internalSelectedIndex = SelectedIndex; } } + + private void OnOrientationChanged() + { + for (int i = 0; i < Items.Count; i++) + { + if (ContainerFromIndex(i) is SegmentedItem item) + { + item.UpdateOrientation(Orientation); + } + } + } } diff --git a/components/Segmented/src/Segmented/Segmented.xaml b/components/Segmented/src/Segmented/Segmented.xaml index ffc5460b..72d8dced 100644 --- a/components/Segmented/src/Segmented/Segmented.xaml +++ b/components/Segmented/src/Segmented/Segmented.xaml @@ -48,6 +48,7 @@ + @@ -58,6 +59,7 @@ @@ -111,7 +113,8 @@ - diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs index 6fc91435..7d46eb63 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.cs +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.cs @@ -11,9 +11,15 @@ namespace CommunityToolkit.WinUI.Controls; public partial class SegmentedItem : ListViewItem { internal const string IconLeftState = "IconLeft"; + internal const string IconTopState = "IconTop"; internal const string IconOnlyState = "IconOnly"; internal const string ContentOnlyState = "ContentOnly"; + internal const string HorizontalState = "Horizontal"; + internal const string VerticalState = "Vertical"; + + private bool _isVertical = false; + /// /// Creates a new instance of . /// @@ -26,8 +32,7 @@ public SegmentedItem() protected override void OnApplyTemplate() { base.OnApplyTemplate(); - OnIconChanged(); - ContentChanged(); + UpdateState(); } /// @@ -36,38 +41,32 @@ protected override void OnApplyTemplate() protected override void OnContentChanged(object oldContent, object newContent) { base.OnContentChanged(oldContent, newContent); - ContentChanged(); - } - - private void ContentChanged() - { - if (Content != null) - { - VisualStateManager.GoToState(this, IconLeftState, true); - } - else - { - VisualStateManager.GoToState(this, IconOnlyState, true); - } + UpdateState(); } /// /// Handles changes to the Icon property. /// - protected virtual void OnIconPropertyChanged(IconElement oldValue, IconElement newValue) + protected virtual void OnIconPropertyChanged(IconElement oldValue, IconElement newValue) => UpdateState(); + + internal void UpdateOrientation(Orientation orientation) { - OnIconChanged(); + _isVertical = orientation is Orientation.Vertical; + UpdateState(); } - private void OnIconChanged() + private void UpdateState() { - if (Icon != null) + string contentState = (Icon is null, Content is null) switch { - VisualStateManager.GoToState(this, IconLeftState, true); - } - else - { - VisualStateManager.GoToState(this, ContentOnlyState, true); - } + (false, false) => _isVertical ? IconTopState : IconLeftState, + (false, true) => IconOnlyState, + (true, false) => ContentOnlyState, + (true, true) => ContentOnlyState, // Invalid state. Treat as content only + }; + + // Update states + VisualStateManager.GoToState(this, contentState, true); + VisualStateManager.GoToState(this, _isVertical ? VerticalState : HorizontalState, true); } } diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml index f0ee77f5..14d436ee 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -405,12 +405,37 @@ + + + + + + + + + + - + + + + + + + + + + + + + + + + + - @@ -634,6 +659,10 @@ + + + + - - - - - + + + + + +