Skip to content

Commit 4ed6d12

Browse files
Avid29AndrewKeepCodingmichael-hawker
authored
Orientation option for Segmented (#747)
* Added Orientation property to EqualPanel * Added orientation to default Segmented style * Fixed orientation binding issue in ButtonSegmentedStyle * Refined default SegmentedItem style * Removed unnecessary namespace reference and simplified OnOrientationChanged logic * Apply typo fixes from code review Co-authored-by: Andrew KeepCoding <andrewkeepcoding@gmail.com> * Removed unneccesary callback registration EqualPanel constructor * Added orientation option to styles sample * Adjusted DefaultSegmentedStyle IconTop style * Fixed alignment issues in SegmentedItem IconTop VisualState * Added vertical behavior for the PivotSegmentedStyle * Addressed simple code style suggestions from Arlo * Replaced SelectAxis method with UVCoord ref struct for mapping coordinates * Removed X/Y coords from UVCoord and changed U/V refs to precalculate * Added Orientation property to EqualPanel * Added orientation to default Segmented style * Fixed orientation binding issue in ButtonSegmentedStyle * Refined default SegmentedItem style * Removed unnecessary namespace reference and simplified OnOrientationChanged logic * Apply typo fixes from code review Co-authored-by: Andrew KeepCoding <andrewkeepcoding@gmail.com> * Removed unneccesary callback registration EqualPanel constructor * Added orientation option to styles sample * Adjusted DefaultSegmentedStyle IconTop style * Fixed alignment issues in SegmentedItem IconTop VisualState * Added vertical behavior for the PivotSegmentedStyle * Addressed simple code style suggestions from Arlo * Replaced SelectAxis method with UVCoord ref struct for mapping coordinates * Removed X/Y coords from UVCoord and changed U/V refs to precalculate * Renamed OnPropertyChanged to OnEqualPanelPropertyChanged * Refactored EqualPanel to not use ref fields --------- Co-authored-by: Andrew KeepCoding <andrewkeepcoding@gmail.com> Co-authored-by: Michael Hawker MSFT (XAML Llama) <24302614+michael-hawker@users.noreply.github.com>
1 parent d30f407 commit 4ed6d12

File tree

10 files changed

+302
-129
lines changed

10 files changed

+302
-129
lines changed

components/Segmented/samples/SegmentedBasicSample.xaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- 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. -->
1+
<!-- 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. -->
22
<Page x:Class="SegmentedExperiment.Samples.SegmentedBasicSample"
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -14,6 +14,7 @@
1414
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}"
1515
Text="Icon + content" />
1616
<controls:Segmented HorizontalAlignment="{x:Bind local:SegmentedBasicSample.ConvertStringToHorizontalAlignment(Alignment), Mode=OneWay}"
17+
Orientation="{x:Bind local:SegmentedBasicSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
1718
SelectedIndex="0"
1819
SelectionMode="{x:Bind local:SegmentedBasicSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}">
1920
<controls:SegmentedItem Content="Item 1"
@@ -30,6 +31,7 @@
3031
Style="{StaticResource BodyStrongTextBlockStyle}"
3132
Text="Icon only" />
3233
<controls:Segmented HorizontalAlignment="{x:Bind local:SegmentedBasicSample.ConvertStringToHorizontalAlignment(Alignment), Mode=OneWay}"
34+
Orientation="{x:Bind local:SegmentedBasicSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
3335
SelectedIndex="2"
3436
SelectionMode="{x:Bind local:SegmentedBasicSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}">
3537
<controls:SegmentedItem Icon="{ui:FontIcon Glyph=&#xE8BF;}"

components/Segmented/samples/SegmentedBasicSample.xaml.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace SegmentedExperiment.Samples;
1111
/// </summary>
1212
[ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")]
1313
[ToolkitSampleMultiChoiceOption("Alignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal alignment")]
14+
[ToolkitSampleMultiChoiceOption("OrientationMode", "Horizontal", "Vertical", Title = "Orientation")]
1415

1516
[ToolkitSample(id: nameof(SegmentedBasicSample), "Basics", description: $"A sample for showing how to create and use a {nameof(Segmented)} custom control.")]
1617
public sealed partial class SegmentedBasicSample : Page
@@ -36,5 +37,12 @@ public SegmentedBasicSample()
3637
"Stretch" => HorizontalAlignment.Stretch,
3738
_ => throw new System.NotImplementedException(),
3839
};
40+
41+
public static Orientation ConvertStringToOrientation(string orientation) => orientation switch
42+
{
43+
"Horizontal" => Orientation.Horizontal,
44+
"Vertical" => Orientation.Vertical,
45+
_ => throw new System.NotImplementedException(),
46+
};
3947
}
4048

components/Segmented/samples/SegmentedStylesSample.xaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- 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. -->
1+
<!-- 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. -->
22
<Page x:Class="SegmentedExperiment.Samples.SegmentedStylesSample"
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -19,7 +19,8 @@
1919
Spacing="8">
2020
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}"
2121
Text="PivotSegmentedStyle" />
22-
<controls:Segmented SelectedIndex="1"
22+
<controls:Segmented Orientation="{x:Bind local:SegmentedStylesSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
23+
SelectedIndex="1"
2324
SelectionMode="{x:Bind local:SegmentedStylesSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}"
2425
Style="{StaticResource PivotSegmentedStyle}">
2526
<controls:SegmentedItem>Item 1</controls:SegmentedItem>
@@ -31,7 +32,8 @@
3132
<TextBlock Margin="0,24,0,0"
3233
Style="{StaticResource BodyStrongTextBlockStyle}"
3334
Text="ButtonSegmentedStyle" />
34-
<controls:Segmented SelectedIndex="0"
35+
<controls:Segmented Orientation="{x:Bind local:SegmentedStylesSample.ConvertStringToOrientation(OrientationMode), Mode=OneWay}"
36+
SelectedIndex="0"
3537
SelectionMode="{x:Bind local:SegmentedStylesSample.ConvertStringToSelectionMode(SelectionMode), Mode=OneWay}"
3638
Style="{StaticResource ButtonSegmentedStyle}">
3739
<controls:SegmentedItem Content="Day"

components/Segmented/samples/SegmentedStylesSample.xaml.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace SegmentedExperiment.Samples;
88
/// An sample that shows how the Segmented control has multiple built-in styles.
99
/// </summary>
1010
[ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")]
11+
[ToolkitSampleMultiChoiceOption("OrientationMode", "Horizontal", "Vertical", Title = "Orientation")]
1112

1213
[ToolkitSample(id: nameof(SegmentedStylesSample), "Additional styles", description: "A sample on how to use different built-in styles.")]
1314
public sealed partial class SegmentedStylesSample : Page
@@ -22,4 +23,11 @@ public SegmentedStylesSample()
2223
"Multiple" => ListViewSelectionMode.Multiple,
2324
_ => throw new System.NotImplementedException(),
2425
};
26+
27+
public static Orientation ConvertStringToOrientation(string orientation) => orientation switch
28+
{
29+
"Horizontal" => Orientation.Horizontal,
30+
"Vertical" => Orientation.Vertical,
31+
_ => throw new System.NotImplementedException(),
32+
};
2533
}

components/Segmented/src/EqualPanel.cs

Lines changed: 125 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@ public partial class EqualPanel : Panel
1414
private double _maxItemWidth = 0;
1515
private double _maxItemHeight = 0;
1616
private int _visibleItemsCount = 0;
17-
18-
/// <summary>
19-
/// Gets or sets the spacing between items.
20-
/// </summary>
21-
public double Spacing
22-
{
23-
get { return (double)GetValue(SpacingProperty); }
24-
set { SetValue(SpacingProperty, value); }
25-
}
2617

2718
/// <summary>
2819
/// Identifies the Spacing dependency property.
@@ -32,14 +23,41 @@ public double Spacing
3223
nameof(Spacing),
3324
typeof(double),
3425
typeof(EqualPanel),
35-
new PropertyMetadata(default(double), OnSpacingChanged));
26+
new PropertyMetadata(default(double), OnEqualPanelPropertyChanged));
27+
28+
/// <summary>
29+
/// Backing <see cref="DependencyProperty"/> for the <see cref="Orientation"/> property.
30+
/// </summary>
31+
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
32+
nameof(Orientation),
33+
typeof(Orientation),
34+
typeof(EqualPanel),
35+
new PropertyMetadata(default(Orientation), OnEqualPanelPropertyChanged));
36+
37+
/// <summary>
38+
/// Gets or sets the spacing between items.
39+
/// </summary>
40+
public double Spacing
41+
{
42+
get => (double)GetValue(SpacingProperty);
43+
set => SetValue(SpacingProperty, value);
44+
}
45+
46+
/// <summary>
47+
/// Gets or sets the panel orientation.
48+
/// </summary>
49+
public Orientation Orientation
50+
{
51+
get => (Orientation)GetValue(OrientationProperty);
52+
set => SetValue(OrientationProperty, value);
53+
}
3654

3755
/// <summary>
3856
/// Creates a new instance of the <see cref="EqualPanel"/> class.
3957
/// </summary>
4058
public EqualPanel()
4159
{
42-
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged);
60+
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnAlignmentChanged);
4361
}
4462

4563
/// <inheritdoc/>
@@ -58,56 +76,129 @@ protected override Size MeasureOverride(Size availableSize)
5876
_maxItemHeight = Math.Max(_maxItemHeight, child.DesiredSize.Height);
5977
}
6078

61-
if (_visibleItemsCount > 0)
79+
// No children, no space taken
80+
if (_visibleItemsCount <= 0)
81+
return new Size(0, 0);
82+
83+
// Determine if the desired alignment is stretched.
84+
// Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash.
85+
bool stretch = Orientation switch
6286
{
63-
// Return equal widths based on the widest item
64-
// In very specific edge cases the AvailableWidth might be infinite resulting in a crash.
65-
if (HorizontalAlignment != HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width))
66-
{
67-
return new Size((_maxItemWidth * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)), _maxItemHeight);
68-
}
69-
else
70-
{
71-
// Equal columns based on the available width, adjust for spacing
72-
double totalWidth = availableSize.Width - (Spacing * (_visibleItemsCount - 1));
73-
_maxItemWidth = totalWidth / _visibleItemsCount;
74-
return new Size(availableSize.Width, _maxItemHeight);
75-
}
87+
Orientation.Horizontal => HorizontalAlignment is HorizontalAlignment.Stretch && !double.IsInfinity(availableSize.Width),
88+
Orientation.Vertical or _ => VerticalAlignment is VerticalAlignment.Stretch && !double.IsInfinity(availableSize.Height),
89+
};
90+
91+
// Define UV coords for orientation agnostic XY manipulation
92+
var uvSize = new UVCoord(0, 0, Orientation);
93+
var maxItemSize = new UVCoord(_maxItemWidth, _maxItemHeight, Orientation);
94+
double availableU = Orientation is Orientation.Horizontal ? availableSize.Width : availableSize.Height;
95+
96+
if (stretch)
97+
{
98+
// Adjust maxItemU to form equal rows/columns by available U space (adjust for spacing)
99+
double totalU = availableU - (Spacing * (_visibleItemsCount - 1));
100+
maxItemSize.U = totalU / _visibleItemsCount;
101+
102+
// Set uSize/vSize for XY result construction
103+
uvSize.U = availableU;
104+
uvSize.V = maxItemSize.V;
76105
}
77106
else
78107
{
79-
return new Size(0, 0);
108+
uvSize.U = (maxItemSize.U * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1));
109+
uvSize.V = maxItemSize.V;
80110
}
111+
112+
return new Size(uvSize.X, uvSize.Y);
81113
}
82114

83115
/// <inheritdoc/>
84116
protected override Size ArrangeOverride(Size finalSize)
85117
{
86-
double x = 0;
87-
118+
// Define UV axis
119+
var pos = new UVCoord(0, 0, Orientation);
120+
ref double maxItemU = ref _maxItemWidth;
121+
double finalSizeU = finalSize.Width;
122+
if (Orientation is Orientation.Vertical)
123+
{
124+
maxItemU = ref _maxItemHeight;
125+
finalSizeU = finalSize.Height;
126+
}
127+
88128
// 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.
89-
if (finalSize.Width > _visibleItemsCount * _maxItemWidth + (Spacing * (_visibleItemsCount - 1)))
129+
if (finalSizeU > _visibleItemsCount * maxItemU + (Spacing * (_visibleItemsCount - 1)))
90130
{
91-
_maxItemWidth = (finalSize.Width - (Spacing * (_visibleItemsCount - 1))) / _visibleItemsCount;
131+
maxItemU = (finalSizeU - (Spacing * (_visibleItemsCount - 1))) / _visibleItemsCount;
92132
}
93133

94134
var elements = Children.Where(static e => e.Visibility == Visibility.Visible);
95135
foreach (var child in elements)
96136
{
97-
child.Arrange(new Rect(x, 0, _maxItemWidth, _maxItemHeight));
98-
x += _maxItemWidth + Spacing;
137+
// NOTE: The arrange method is still in X/Y coordinate system
138+
child.Arrange(new Rect(pos.X, pos.Y, _maxItemWidth, _maxItemHeight));
139+
pos.U += maxItemU + Spacing;
99140
}
100141
return finalSize;
101142
}
102143

103-
private void OnHorizontalAlignmentChanged(DependencyObject sender, DependencyProperty dp)
144+
private void OnAlignmentChanged(DependencyObject sender, DependencyProperty dp)
104145
{
105146
InvalidateMeasure();
106147
}
107148

108-
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
149+
private static void OnEqualPanelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
109150
{
110151
var panel = (EqualPanel)d;
111152
panel.InvalidateMeasure();
112153
}
154+
155+
/// <summary>
156+
/// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system.
157+
/// </summary>
158+
private struct UVCoord(double x, double y, Orientation orientation)
159+
{
160+
private readonly bool _horizontal = orientation is Orientation.Horizontal;
161+
162+
public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation)
163+
{
164+
}
165+
166+
public double X { get; set; } = x;
167+
168+
public double Y { get; set; } = y;
169+
170+
public double U
171+
{
172+
readonly get => _horizontal ? X : Y;
173+
set
174+
{
175+
if (_horizontal)
176+
{
177+
X = value;
178+
}
179+
else
180+
{
181+
Y = value;
182+
}
183+
}
184+
}
185+
186+
public double V
187+
{
188+
readonly get => _horizontal ? Y : X;
189+
set
190+
{
191+
if (_horizontal)
192+
{
193+
Y = value;
194+
}
195+
else
196+
{
197+
X = value;
198+
}
199+
}
200+
}
201+
202+
public readonly Size Size => new(X, Y);
203+
}
113204
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace CommunityToolkit.WinUI.Controls;
6+
7+
public partial class Segmented
8+
{
9+
/// <summary>
10+
/// The backing <see cref="DependencyProperty"/> for the <see cref="Orientation"/> property.
11+
/// </summary>
12+
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
13+
nameof(Orientation),
14+
typeof(Orientation),
15+
typeof(Segmented),
16+
new PropertyMetadata(Orientation.Horizontal, (d, e) => ((Segmented)d).OnOrientationChanged()));
17+
18+
/// <summary>
19+
/// Gets or sets the orientation.
20+
/// </summary>
21+
public Orientation Orientation
22+
{
23+
get => (Orientation)GetValue(OrientationProperty);
24+
set => SetValue(OrientationProperty, value);
25+
}
26+
}

components/Segmented/src/Segmented/Segmented.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public Segmented()
2222
this.DefaultStyleKey = typeof(Segmented);
2323

2424
RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged);
25+
RegisterPropertyChangedCallback(OrientationProperty, OnSelectedIndexChanged);
2526
}
2627

2728
/// <inheritdoc/>
@@ -150,4 +151,15 @@ private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty
150151
_internalSelectedIndex = SelectedIndex;
151152
}
152153
}
154+
155+
private void OnOrientationChanged()
156+
{
157+
for (int i = 0; i < Items.Count; i++)
158+
{
159+
if (ContainerFromIndex(i) is SegmentedItem item)
160+
{
161+
item.UpdateOrientation(Orientation);
162+
}
163+
}
164+
}
153165
}

components/Segmented/src/Segmented/Segmented.xaml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33
xmlns:local="using:CommunityToolkit.WinUI.Controls"
44
xmlns:tk="using:CommunityToolkit.WinUI"
@@ -48,6 +48,7 @@
4848
<Setter Property="HorizontalAlignment" Value="Left" />
4949
<Setter Property="VerticalAlignment" Value="Center" />
5050
<Setter Property="SelectionMode" Value="Single" />
51+
<Setter Property="Orientation" Value="Horizontal" />
5152
<Setter Property="IsItemClickEnabled" Value="False" />
5253
<win:Setter Property="SingleSelectionFollowsFocus"
5354
Value="False" />
@@ -58,6 +59,7 @@
5859
<ItemsPanelTemplate>
5960
<local:EqualPanel HorizontalAlignment="{Binding (tk:FrameworkElementExtensions.Ancestor).HorizontalAlignment, RelativeSource={RelativeSource Self}}"
6061
tk:FrameworkElementExtensions.AncestorType="local:Segmented"
62+
Orientation="{Binding (tk:FrameworkElementExtensions.Ancestor).Orientation, RelativeSource={RelativeSource Self}}"
6163
Spacing="{ThemeResource SegmentedItemSpacing}" />
6264
</ItemsPanelTemplate>
6365
</Setter.Value>
@@ -91,7 +93,8 @@
9193
<Setter Property="ItemsPanel">
9294
<Setter.Value>
9395
<ItemsPanelTemplate>
94-
<StackPanel Orientation="Horizontal"
96+
<StackPanel tk:FrameworkElementExtensions.AncestorType="local:Segmented"
97+
Orientation="{Binding (tk:FrameworkElementExtensions.Ancestor).Orientation, RelativeSource={RelativeSource Self}}"
9598
Spacing="{ThemeResource SegmentedItemSpacing}" />
9699
</ItemsPanelTemplate>
97100
</Setter.Value>
@@ -111,7 +114,8 @@
111114
<Setter Property="ItemsPanel">
112115
<Setter.Value>
113116
<ItemsPanelTemplate>
114-
<StackPanel Orientation="Horizontal"
117+
<StackPanel tk:FrameworkElementExtensions.AncestorType="local:Segmented"
118+
Orientation="{Binding (tk:FrameworkElementExtensions.Ancestor).Orientation, RelativeSource={RelativeSource Self}}"
115119
Spacing="{ThemeResource ButtonItemSpacing}" />
116120
</ItemsPanelTemplate>
117121
</Setter.Value>

0 commit comments

Comments
 (0)