diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml b/components/Segmented/samples/SegmentedBasicSample.xaml index f00f791e..51ef53b1 100644 --- a/components/Segmented/samples/SegmentedBasicSample.xaml +++ b/components/Segmented/samples/SegmentedBasicSample.xaml @@ -1,4 +1,4 @@ - + [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/samples/SegmentedStylesSample.xaml b/components/Segmented/samples/SegmentedStylesSample.xaml index 72b1b75b..7a2b20d0 100644 --- a/components/Segmented/samples/SegmentedStylesSample.xaml +++ b/components/Segmented/samples/SegmentedStylesSample.xaml @@ -1,4 +1,4 @@ - + - Item 1 @@ -31,7 +32,8 @@ - [ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")] +[ToolkitSampleMultiChoiceOption("OrientationMode", "Horizontal", "Vertical", Title = "Orientation")] [ToolkitSample(id: nameof(SegmentedStylesSample), "Additional styles", description: "A sample on how to use different built-in styles.")] public sealed partial class SegmentedStylesSample : Page @@ -22,4 +23,11 @@ public SegmentedStylesSample() "Multiple" => ListViewSelectionMode.Multiple, _ => 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..4f01131f 100644 --- a/components/Segmented/src/EqualPanel.cs +++ b/components/Segmented/src/EqualPanel.cs @@ -14,15 +14,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 +23,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); } /// @@ -58,56 +76,113 @@ protected override Size MeasureOverride(Size availableSize) _maxItemHeight = Math.Max(_maxItemHeight, child.DesiredSize.Height); } - if (_visibleItemsCount > 0) + // No children, no space taken + if (_visibleItemsCount <= 0) + return new Size(0, 0); + + // Determine if the desired alignment is stretched. + // Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash. + bool stretch = Orientation switch { - // 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)) - { - return new Size((_maxItemWidth * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)), _maxItemHeight); - } - 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); - } + 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 + var uvSize = new UVCoord(ref xSize, ref ySize, Orientation); + var maxItemSize = new UVCoord(ref _maxItemWidth, ref _maxItemHeight, Orientation); + double availableU = Orientation is Orientation.Horizontal ? availableSize.Width : availableSize.Height; + + if (stretch) + { + // Adjust maxItemU to form equal rows/columns by available U space (adjust for spacing) + double totalU = availableU - (Spacing * (_visibleItemsCount - 1)); + maxItemSize.U = totalU / _visibleItemsCount; + + // Set uSize/vSize for XY result construction + uvSize.U = availableU; + uvSize.V = maxItemSize.V; } else { - return new Size(0, 0); + uvSize.U = (maxItemSize.U * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)); + uvSize.V = maxItemSize.V; } + + return new Size(xSize, ySize); } /// protected override Size ArrangeOverride(Size finalSize) { + // Define X and Y double x = 0; + double y = 0; + // Define UV axis + var pos = new UVCoord(ref x, ref y, Orientation); + ref double maxItemU = ref _maxItemWidth; + double finalSizeU = finalSize.Width; + if (Orientation is Orientation.Vertical) + { + 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)); + pos.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(); } + + /// + /// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system. + /// + private readonly ref struct UVCoord + { + private readonly ref double _u; + private readonly ref double _v; + + public UVCoord(ref double x, ref double y, Orientation orientation) + { + if (orientation is Orientation.Horizontal) + { + _u = ref x; + _v = ref y; + } + else + { + _u = ref y; + _v = ref x; + } + } + + public readonly ref double U => ref _u; + + public readonly ref double V => ref _v; + } } 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..b7166894 100644 --- a/components/Segmented/src/Segmented/Segmented.xaml +++ b/components/Segmented/src/Segmented/Segmented.xaml @@ -1,4 +1,4 @@ - + @@ -58,6 +59,7 @@ @@ -91,7 +93,8 @@ - @@ -111,7 +114,8 @@ - diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs index 6fc91435..346d4dd9 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(); + UpdateVisualStates(); } /// @@ -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); - } + UpdateVisualStates(); } /// /// Handles changes to the Icon property. /// - protected virtual void OnIconPropertyChanged(IconElement oldValue, IconElement newValue) + protected virtual void OnIconPropertyChanged(IconElement oldValue, IconElement newValue) => UpdateVisualStates(); + + internal void UpdateOrientation(Orientation orientation) { - OnIconChanged(); + _isVertical = orientation is Orientation.Vertical; + UpdateVisualStates(); } - private void OnIconChanged() + private void UpdateVisualStates() { - 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 visual 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..6f7d97c5 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -1,4 +1,4 @@ - @@ -405,19 +405,42 @@ + + + + + + + + + + - + + + + + + + + + + + + + + + + + - - - @@ -625,15 +648,12 @@ - - - - - + - - - - - - + + + + + + + @@ -693,19 +712,26 @@ Control.IsTemplateFocusTarget="True" CornerRadius="{TemplateBinding CornerRadius}"> + + + + + + + + + + - + - - - @@ -858,15 +884,11 @@ - - - - - + - - - - - - + + + + + + +