Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions components/Segmented/samples/SegmentedBasicSample.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
}

111 changes: 86 additions & 25 deletions components/Segmented/src/EqualPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,15 +15,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 +24,41 @@ public double Spacing
nameof(Spacing),
typeof(double),
typeof(EqualPanel),
new PropertyMetadata(default(double), OnSpacingChanged));
new PropertyMetadata(default(double), OnPropertyChanged));

/// <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), OnPropertyChanged));

/// <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 @@ -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
{
Expand All @@ -83,31 +122,53 @@ protected override Size MeasureOverride(Size availableSize)
/// <inheritdoc/>
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;
}
}
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);
}
}
}
}
5 changes: 4 additions & 1 deletion components/Segmented/src/Segmented/Segmented.xaml
Original file line number Diff line number Diff line change
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 @@ -111,7 +113,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
49 changes: 24 additions & 25 deletions components/Segmented/src/SegmentedItem/SegmentedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Creates a new instance of <see cref="SegmentedItem"/>.
/// </summary>
Expand All @@ -26,8 +32,7 @@ public SegmentedItem()
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
OnIconChanged();
ContentChanged();
UpdateState();
}

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

/// <summary>
/// Handles changes to the Icon property.
/// </summary>
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);
}
}
Loading
Loading