Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
25106dc
Added Orientation property to EqualPanel
Avid29 Nov 17, 2025
4d35ca1
Added orientation to default Segmented style
Avid29 Nov 17, 2025
99738b1
Fixed orientation binding issue in ButtonSegmentedStyle
Avid29 Nov 17, 2025
c4e0ae3
Refined default SegmentedItem style
Avid29 Nov 17, 2025
093c4b2
Removed unnecessary namespace reference and simplified OnOrientationC…
Avid29 Nov 18, 2025
9924e89
Apply typo fixes from code review
Avid29 Nov 19, 2025
00de4bc
Removed unneccesary callback registration EqualPanel constructor
Avid29 Nov 19, 2025
fccfc84
Added orientation option to styles sample
Avid29 Nov 26, 2025
db22dd1
Adjusted DefaultSegmentedStyle IconTop style
Avid29 Nov 26, 2025
aa68ff5
Fixed alignment issues in SegmentedItem IconTop VisualState
Avid29 Nov 26, 2025
fe533f5
Added vertical behavior for the PivotSegmentedStyle
Avid29 Nov 26, 2025
4b3fb0e
Addressed simple code style suggestions from Arlo
Avid29 Nov 26, 2025
05c7684
Replaced SelectAxis method with UVCoord ref struct for mapping coordi…
Avid29 Nov 26, 2025
bb11f05
Removed X/Y coords from UVCoord and changed U/V refs to precalculate
Avid29 Nov 28, 2025
da9651d
Added Orientation property to EqualPanel
Avid29 Nov 17, 2025
59211cd
Added orientation to default Segmented style
Avid29 Nov 17, 2025
6608ac4
Fixed orientation binding issue in ButtonSegmentedStyle
Avid29 Nov 17, 2025
262b9ce
Refined default SegmentedItem style
Avid29 Nov 17, 2025
ad01937
Removed unnecessary namespace reference and simplified OnOrientationC…
Avid29 Nov 18, 2025
8e06bb9
Apply typo fixes from code review
Avid29 Nov 19, 2025
c4013af
Removed unneccesary callback registration EqualPanel constructor
Avid29 Nov 19, 2025
74a7c07
Added orientation option to styles sample
Avid29 Nov 26, 2025
21614d6
Adjusted DefaultSegmentedStyle IconTop style
Avid29 Nov 26, 2025
6f045eb
Fixed alignment issues in SegmentedItem IconTop VisualState
Avid29 Nov 26, 2025
70348bb
Added vertical behavior for the PivotSegmentedStyle
Avid29 Nov 26, 2025
702d026
Addressed simple code style suggestions from Arlo
Avid29 Nov 26, 2025
7d38879
Replaced SelectAxis method with UVCoord ref struct for mapping coordi…
Avid29 Nov 26, 2025
b2bf5af
Removed X/Y coords from UVCoord and changed U/V refs to precalculate
Avid29 Nov 28, 2025
0d72bac
Renamed OnPropertyChanged to OnEqualPanelPropertyChanged
Avid29 Dec 4, 2025
1c131f6
Merge
Avid29 Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/Segmented/samples/SegmentedBasicSample.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- 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. -->
<!-- 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. -->
<Page x:Class="SegmentedExperiment.Samples.SegmentedBasicSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Expand All @@ -14,6 +14,7 @@
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}"
Text="Icon + content" />
<controls:Segmented HorizontalAlignment="{x:Bind local:SegmentedBasicSample.ConvertStringToHorizontalAlignment(Alignment), Mode=OneWay}"
Orientation="{x:Bind local:SegmentedBasicSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
SelectedIndex="0"
SelectionMode="{x:Bind local:SegmentedBasicSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}">
<controls:SegmentedItem Content="Item 1"
Expand All @@ -30,6 +31,7 @@
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="Icon only" />
<controls:Segmented HorizontalAlignment="{x:Bind local:SegmentedBasicSample.ConvertStringToHorizontalAlignment(Alignment), Mode=OneWay}"
Orientation="{x:Bind local:SegmentedBasicSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
SelectedIndex="2"
SelectionMode="{x:Bind local:SegmentedBasicSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}">
<controls:SegmentedItem Icon="{ui:FontIcon Glyph=&#xE8BF;}"
Expand Down
8 changes: 8 additions & 0 deletions components/Segmented/samples/SegmentedBasicSample.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace SegmentedExperiment.Samples;
/// </summary>
[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
Expand All @@ -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(),
};
}

8 changes: 5 additions & 3 deletions components/Segmented/samples/SegmentedStylesSample.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- 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. -->
<!-- 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. -->
<Page x:Class="SegmentedExperiment.Samples.SegmentedStylesSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Expand All @@ -19,7 +19,8 @@
Spacing="8">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}"
Text="PivotSegmentedStyle" />
<controls:Segmented SelectedIndex="1"
<controls:Segmented Orientation="{x:Bind local:SegmentedStylesSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
SelectedIndex="1"
SelectionMode="{x:Bind local:SegmentedStylesSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}"
Style="{StaticResource PivotSegmentedStyle}">
<controls:SegmentedItem>Item 1</controls:SegmentedItem>
Expand All @@ -31,7 +32,8 @@
<TextBlock Margin="0,24,0,0"
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="ButtonSegmentedStyle" />
<controls:Segmented SelectedIndex="0"
<controls:Segmented Orientation="{x:Bind local:SegmentedStylesSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
SelectedIndex="0"
SelectionMode="{x:Bind local:SegmentedStylesSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}"
Style="{StaticResource ButtonSegmentedStyle}">
<controls:SegmentedItem Content="Day"
Expand Down
8 changes: 8 additions & 0 deletions components/Segmented/samples/SegmentedStylesSample.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace SegmentedExperiment.Samples;
/// An sample that shows how the Segmented control has multiple built-in styles.
/// </summary>
[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
Expand All @@ -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(),
};
}
139 changes: 107 additions & 32 deletions components/Segmented/src/EqualPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,6 @@ public partial class EqualPanel : Panel
private double _maxItemWidth = 0;
private double _maxItemHeight = 0;
private int _visibleItemsCount = 0;

/// <summary>
/// Gets or sets the spacing between items.
/// </summary>
public double Spacing
{
get { return (double)GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}

/// <summary>
/// Identifies the Spacing dependency property.
Expand All @@ -32,14 +23,41 @@ public double Spacing
nameof(Spacing),
typeof(double),
typeof(EqualPanel),
new PropertyMetadata(default(double), OnSpacingChanged));
new PropertyMetadata(default(double), OnEqualPanelPropertyChanged));

/// <summary>
/// Backing <see cref="DependencyProperty"/> for the <see cref="Orientation"/> property.
/// </summary>
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation),
typeof(Orientation),
typeof(EqualPanel),
new PropertyMetadata(default(Orientation), OnEqualPanelPropertyChanged));

/// <summary>
/// Gets or sets the spacing between items.
/// </summary>
public double Spacing
{
get => (double)GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}

/// <summary>
/// Gets or sets the panel orientation.
/// </summary>
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}

/// <summary>
/// Creates a new instance of the <see cref="EqualPanel"/> class.
/// </summary>
public EqualPanel()
{
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged);
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnAlignmentChanged);
}

/// <inheritdoc/>
Expand All @@ -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);
}

/// <inheritdoc/>
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 OnEqualPanelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (EqualPanel)d;
panel.InvalidateMeasure();
}

/// <summary>
/// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system.
/// </summary>
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;
}
}
26 changes: 26 additions & 0 deletions components/Segmented/src/Segmented/Segmented.Properties.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The backing <see cref="DependencyProperty"/> for the <see cref="Orientation"/> property.
/// </summary>
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation),
typeof(Orientation),
typeof(Segmented),
new PropertyMetadata(Orientation.Horizontal, (d, e) => ((Segmented)d).OnOrientationChanged()));

/// <summary>
/// Gets or sets the orientation.
/// </summary>
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
}
12 changes: 12 additions & 0 deletions components/Segmented/src/Segmented/Segmented.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public Segmented()
this.DefaultStyleKey = typeof(Segmented);

RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
RegisterPropertyChangedCallback(OrientationProperty, OnSelectedIndexChanged);
Comment on lines 24 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to extract the logic in OnSelectedIndexChanged into a SaveInitialIndex method then call it from OnSelectedIndexChanged and OnOrientationChanged?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, wait a minute. That's supposed to call OnOrientationChanged... Yet it does work. Why is that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. Because I moved the callback to the DependencyProperty declaration. This line is entirely unnecessary.

}

/// <inheritdoc/>
Expand Down Expand Up @@ -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);
}
}
}
}
10 changes: 7 additions & 3 deletions components/Segmented/src/Segmented/Segmented.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CommunityToolkit.WinUI.Controls"
xmlns:tk="using:CommunityToolkit.WinUI"
Expand Down Expand Up @@ -48,6 +48,7 @@
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="SelectionMode" Value="Single" />
<Setter Property="Orientation" Value="Horizontal" />
<Setter Property="IsItemClickEnabled" Value="False" />
<win:Setter Property="SingleSelectionFollowsFocus"
Value="False" />
Expand All @@ -58,6 +59,7 @@
<ItemsPanelTemplate>
<local:EqualPanel HorizontalAlignment="{Binding (tk:FrameworkElementExtensions.Ancestor).HorizontalAlignment, RelativeSource={RelativeSource Self}}"
tk:FrameworkElementExtensions.AncestorType="local:Segmented"
Orientation="{Binding (tk:FrameworkElementExtensions.Ancestor).Orientation, RelativeSource={RelativeSource Self}}"
Spacing="{ThemeResource SegmentedItemSpacing}" />
</ItemsPanelTemplate>
</Setter.Value>
Expand Down Expand Up @@ -91,7 +93,8 @@
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
<StackPanel tk:FrameworkElementExtensions.AncestorType="local:Segmented"
Orientation="{Binding (tk:FrameworkElementExtensions.Ancestor).Orientation, RelativeSource={RelativeSource Self}}"
Spacing="{ThemeResource SegmentedItemSpacing}" />
</ItemsPanelTemplate>
</Setter.Value>
Expand All @@ -111,7 +114,8 @@
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
<StackPanel tk:FrameworkElementExtensions.AncestorType="local:Segmented"
Orientation="{Binding (tk:FrameworkElementExtensions.Ancestor).Orientation, RelativeSource={RelativeSource Self}}"
Spacing="{ThemeResource ButtonItemSpacing}" />
</ItemsPanelTemplate>
</Setter.Value>
Expand Down
Loading
Loading