-
Notifications
You must be signed in to change notification settings - Fork 123
Orientation option for Segmented #747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
25106dc
4d35ca1
99738b1
c4e0ae3
093c4b2
9924e89
00de4bc
fccfc84
db22dd1
aa68ff5
fe533f5
4b3fb0e
05c7684
bb11f05
da9651d
59211cd
6608ac4
262b9ce
ad01937
8e06bb9
c4013af
74a7c07
21614d6
6f045eb
70348bb
702d026
7d38879
b2bf5af
0d72bac
1c131f6
7c59417
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
| /// <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. | ||
|
|
@@ -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/> | ||
|
|
@@ -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); | ||
Arlodotexe marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| { | ||
|
|
@@ -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; | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ public Segmented() | |
| this.DefaultStyleKey = typeof(Segmented); | ||
|
|
||
| RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged); | ||
| RegisterPropertyChangedCallback(OrientationProperty, OnSelectedIndexChanged); | ||
|
Comment on lines
24
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to extract the logic in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, wait a minute. That's supposed to call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/> | ||
|
|
@@ -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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
|
@@ -26,8 +32,7 @@ public SegmentedItem() | |
| protected override void OnApplyTemplate() | ||
| { | ||
| base.OnApplyTemplate(); | ||
| OnIconChanged(); | ||
| ContentChanged(); | ||
| UpdateState(); | ||
|
||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name
OnPropertyChangedis less specific because it's being called from multiple places, but it needs to be disambiguated between the other properties and property changed callbacks here.I suggest removing the second unused param from this method (discarding it w/
DependencyPropertyChangedEventArgs _in a delegate instead) and naming the method based on what it does, rather than naming based on where it's called from.In fact, since all it's doing is calling
InvalidateMeasure()on the first paramDependencyObject dcast toEqualPanel, we could probably just inline delegate that call without splitting into a new method.Which solution we choose depends on whether we expect code to accumulate in these callback bodies. Seems like a straightforward
.InvalidateMeasure()on whichever dependency property container called it, so I'm leaning towards the second delegate-only option.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it need to be disambiguated? Every property actually defined on
EqualPaneluses this callback, and the only reasonOnAlignmentChangedis different is because it required a different delegate by nature of being a property of a parent type.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because other DP change callbacks exist in this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. I'll change the name to
OnEqualPanelPropertyChanged, since all properties which actually belong to theEqualPaneluse this callback.