From 25106dcd908652abf4f8de2d4853084aac1c33f2 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Mon, 17 Nov 2025 17:08:12 +0200 Subject: [PATCH 1/7] Added Orientation property to EqualPanel --- components/Segmented/src/EqualPanel.cs | 112 +++++++++++++++++++------ 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/components/Segmented/src/EqualPanel.cs b/components/Segmented/src/EqualPanel.cs index 3caa7cbf..d2befe6f 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,42 @@ 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); + RegisterPropertyChangedCallback(VerticalAlignmentProperty, OnAlignmentChanged); } /// @@ -60,19 +80,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 contstruction + 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 +123,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; + } } From 4d35ca1cf80b45dbc916c3374df55075304fa46a Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Mon, 17 Nov 2025 19:24:59 +0200 Subject: [PATCH 2/7] Added orientation to default Segmented style --- .../samples/SegmentedBasicSample.xaml | 2 + .../samples/SegmentedBasicSample.xaml.cs | 8 +++ .../src/Segmented/Segmented.Properties.cs | 26 ++++++++ .../Segmented/src/Segmented/Segmented.cs | 13 ++++ .../Segmented/src/Segmented/Segmented.xaml | 3 +- .../src/SegmentedItem/SegmentedItem.cs | 51 +++++++-------- .../src/SegmentedItem/SegmentedItem.xaml | 63 +++++++++++++------ 7 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 components/Segmented/src/Segmented/Segmented.Properties.cs 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/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..982eaa94 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,16 @@ private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty _internalSelectedIndex = SelectedIndex; } } + + private void OnOrientationChanged() + { + for (int i = 0; i < Items.Count; i++) + { + var container = ContainerFromIndex(i) as SegmentedItem; + if (container is null) + continue; + + container.UpdateOrientation(Orientation); + } + } } diff --git a/components/Segmented/src/Segmented/Segmented.xaml b/components/Segmented/src/Segmented/Segmented.xaml index ffc5460b..960bcfc6 100644 --- a/components/Segmented/src/Segmented/Segmented.xaml +++ b/components/Segmented/src/Segmented/Segmented.xaml @@ -58,6 +58,7 @@ @@ -111,7 +112,7 @@ - diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs index 6fc91435..a6145ee4 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.cs +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.cs @@ -2,6 +2,8 @@ // 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.Media.Devices; + namespace CommunityToolkit.WinUI.Controls; /// @@ -11,9 +13,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 +34,7 @@ public SegmentedItem() protected override void OnApplyTemplate() { base.OnApplyTemplate(); - OnIconChanged(); - ContentChanged(); + UpdateState(); } /// @@ -36,38 +43,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. Treast 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..c17de8be 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -1,4 +1,4 @@ - @@ -405,12 +405,33 @@ + + + + + + + + + + - + + + + + + + + + + + + + - @@ -634,6 +655,10 @@ + + + + - - - - - + + + + + + From 99738b14262f71c2c96f5e55f946eede62d66a2a Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Mon, 17 Nov 2025 23:10:23 +0200 Subject: [PATCH 3/7] Fixed orientation binding issue in ButtonSegmentedStyle --- components/Segmented/src/Segmented/Segmented.xaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/Segmented/src/Segmented/Segmented.xaml b/components/Segmented/src/Segmented/Segmented.xaml index 960bcfc6..72d8dced 100644 --- a/components/Segmented/src/Segmented/Segmented.xaml +++ b/components/Segmented/src/Segmented/Segmented.xaml @@ -48,6 +48,7 @@ + @@ -112,7 +113,8 @@ - From c4e0ae3bfb8cafed8186f3519e6150266ad700ae Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Mon, 17 Nov 2025 23:11:41 +0200 Subject: [PATCH 4/7] Refined default SegmentedItem style --- components/Segmented/src/SegmentedItem/SegmentedItem.xaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml index c17de8be..14d436ee 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -1,4 +1,4 @@ - @@ -419,10 +419,14 @@ + + - + + + From 093c4b22cde48326199940027eb4bf70afcba43d Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 18 Nov 2025 17:00:19 +0200 Subject: [PATCH 5/7] Removed unnecessary namespace reference and simplified OnOrientationChanged logic --- components/Segmented/src/Segmented/Segmented.cs | 9 ++++----- components/Segmented/src/SegmentedItem/SegmentedItem.cs | 2 -- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/components/Segmented/src/Segmented/Segmented.cs b/components/Segmented/src/Segmented/Segmented.cs index 982eaa94..938dca8e 100644 --- a/components/Segmented/src/Segmented/Segmented.cs +++ b/components/Segmented/src/Segmented/Segmented.cs @@ -160,11 +160,10 @@ private void OnOrientationChanged() { for (int i = 0; i < Items.Count; i++) { - var container = ContainerFromIndex(i) as SegmentedItem; - if (container is null) - continue; - - container.UpdateOrientation(Orientation); + if (ContainerFromIndex(i) is SegmentedItem item) + { + item.UpdateOrientation(Orientation); + } } } } diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs index a6145ee4..61cdb58b 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.cs +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.cs @@ -2,8 +2,6 @@ // 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.Media.Devices; - namespace CommunityToolkit.WinUI.Controls; /// From 9924e892fd760c4be999cef46427013d49c37432 Mon Sep 17 00:00:00 2001 From: Avishai Dernis Date: Wed, 19 Nov 2025 02:41:33 +0200 Subject: [PATCH 6/7] Apply typo fixes from code review Co-authored-by: Andrew KeepCoding --- components/Segmented/src/EqualPanel.cs | 2 +- components/Segmented/src/SegmentedItem/SegmentedItem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Segmented/src/EqualPanel.cs b/components/Segmented/src/EqualPanel.cs index d2befe6f..86ed8491 100644 --- a/components/Segmented/src/EqualPanel.cs +++ b/components/Segmented/src/EqualPanel.cs @@ -102,7 +102,7 @@ protected override Size MeasureOverride(Size availableSize) double totalU = availableU - (Spacing * (_visibleItemsCount - 1)); maxItemU = totalU / _visibleItemsCount; - // Set uSize/vSize for XY result contstruction + // Set uSize/vSize for XY result construction uSize = availableU; vSize = maxItemV; } diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs index 61cdb58b..7d46eb63 100644 --- a/components/Segmented/src/SegmentedItem/SegmentedItem.cs +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.cs @@ -62,7 +62,7 @@ private void UpdateState() (false, false) => _isVertical ? IconTopState : IconLeftState, (false, true) => IconOnlyState, (true, false) => ContentOnlyState, - (true, true) => ContentOnlyState, // Invalid state. Treast as content only + (true, true) => ContentOnlyState, // Invalid state. Treat as content only }; // Update states From 00de4bcae5d5e04edac927bfd01a9bb6bfdb189e Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Wed, 19 Nov 2025 02:42:42 +0200 Subject: [PATCH 7/7] Removed unneccesary callback registration EqualPanel constructor --- components/Segmented/src/EqualPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Segmented/src/EqualPanel.cs b/components/Segmented/src/EqualPanel.cs index 86ed8491..88b4ba11 100644 --- a/components/Segmented/src/EqualPanel.cs +++ b/components/Segmented/src/EqualPanel.cs @@ -59,7 +59,6 @@ public Orientation Orientation public EqualPanel() { RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnAlignmentChanged); - RegisterPropertyChangedCallback(VerticalAlignmentProperty, OnAlignmentChanged); } ///