diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml index 6d66c19c96..f3098dee0f 100644 --- a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml @@ -8,6 +8,7 @@ M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z + M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z @@ -33,6 +34,18 @@ IsChecked="True" IsCheckedChanged="OnDynamicOverflowChanged" /> + + + + + + @@ -47,11 +60,11 @@ - - + + + + + + + diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs index 45a1c95527..19a451b55c 100644 --- a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs +++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs @@ -9,7 +9,13 @@ namespace ControlCatalog.Pages public CommandBarDynamicOverflowPage() { InitializeComponent(); + if (SecondaryVisibleCheck.IsChecked != true) + { + DemoBar.SecondaryCommands?.Remove(DemoSecondaryCommand); + } + ((INotifyCollectionChanged)DemoBar.OverflowItems).CollectionChanged += OnOverflowChanged; + ((INotifyCollectionChanged)DemoBar.VisiblePrimaryCommands).CollectionChanged += OnOverflowChanged; UpdateStatus(); } @@ -29,6 +35,26 @@ namespace ControlCatalog.Pages DemoBar.IsDynamicOverflowEnabled = DynamicOverflowCheck.IsChecked == true; } + private void OnSecondaryVisibilityChanged(object? sender, RoutedEventArgs e) + { + if (DemoBar?.SecondaryCommands == null || DemoSecondaryCommand == null) + return; + + bool shouldInclude = SecondaryVisibleCheck.IsChecked == true; + bool isIncluded = DemoBar.SecondaryCommands.Contains(DemoSecondaryCommand); + + if (shouldInclude && !isIncluded) + { + DemoBar.SecondaryCommands.Add(DemoSecondaryCommand); + } + else if (!shouldInclude && isIncluded) + { + DemoBar.SecondaryCommands.Remove(DemoSecondaryCommand); + } + + UpdateStatus(); + } + private void OnOverflowChanged(object? sender, NotifyCollectionChangedEventArgs e) { UpdateStatus(); @@ -36,10 +62,40 @@ namespace ControlCatalog.Pages private void UpdateStatus() { - var total = DemoBar.PrimaryCommands.Count; - var overflow = DemoBar.OverflowItems.Count; - var visible = total - overflow; - StatusText.Text = $"Showing {visible} of {total} commands, {overflow} in overflow"; + int visiblePrimaryCommandCount = 0; + int visiblePrimarySeparatorCount = 0; + foreach (var item in DemoBar.VisiblePrimaryCommands) + { + if (item is CommandBarSeparator) + visiblePrimarySeparatorCount++; + else + visiblePrimaryCommandCount++; + } + + int overflowCommandCount = 0; + int overflowSeparatorCount = 0; + bool hasSyntheticOverflowDivider = false; + foreach (var item in DemoBar.OverflowItems) + { + if (item is CommandBarSeparator separator) + { + overflowSeparatorCount++; + if (!DemoBar.PrimaryCommands.Contains(separator) && + !DemoBar.SecondaryCommands.Contains(separator)) + { + hasSyntheticOverflowDivider = true; + } + } + else + { + overflowCommandCount++; + } + } + + StatusText.Text = + $"Visible primary: {visiblePrimaryCommandCount} commands, {visiblePrimarySeparatorCount} separators\n" + + $"Overflow items: {overflowCommandCount} commands, {overflowSeparatorCount} separators\n" + + $"Synthetic overflow divider: {(hasSyntheticOverflowDivider ? "present" : "absent")}"; } } } diff --git a/src/Avalonia.Controls/CommandBar/CommandBar.cs b/src/Avalonia.Controls/CommandBar/CommandBar.cs index d8047f9d9e..6b92a935ad 100644 --- a/src/Avalonia.Controls/CommandBar/CommandBar.cs +++ b/src/Avalonia.Controls/CommandBar/CommandBar.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.Metadata; +using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia.Controls @@ -136,6 +137,8 @@ namespace Avalonia.Controls private readonly ObservableCollection _visiblePrimaryCommands = new(); private readonly ObservableCollection _overflowItems = new(); + private readonly CommandBarSeparator _overflowPrimarySecondarySeparator = new(); + private readonly CompositeDisposable _secondaryCommandVisibilitySubscriptions = new(); private bool _isDynamicUpdateInProgress; private double _constraintWidth = double.PositiveInfinity; private bool _openedViaKeyboard; @@ -151,6 +154,7 @@ namespace Avalonia.Controls var secondaryCommands = new ObservableCollection(); SetCurrentValue(SecondaryCommandsProperty, (IList)secondaryCommands); + RebuildSecondaryCommandVisibilitySubscriptions(); SizeChanged += CommandBar_SizeChanged; } @@ -386,6 +390,18 @@ namespace Avalonia.Controls UpdateDynamicOverflow(); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + RebuildSecondaryCommandVisibilitySubscriptions(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _secondaryCommandVisibilitySubscriptions.Clear(); + base.OnDetachedFromVisualTree(e); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -439,6 +455,7 @@ namespace Avalonia.Controls oldSecondary.CollectionChanged -= OnSecondaryCommandsChanged; if (change.NewValue is INotifyCollectionChanged newSecondary) newSecondary.CollectionChanged += OnSecondaryCommandsChanged; + RebuildSecondaryCommandVisibilitySubscriptions(); UpdateDynamicOverflow(); } } @@ -583,6 +600,7 @@ namespace Avalonia.Controls private void OnSecondaryCommandsChanged(object? sender, NotifyCollectionChangedEventArgs e) { + RebuildSecondaryCommandVisibilitySubscriptions(); UpdateDynamicOverflow(); } @@ -598,12 +616,9 @@ namespace Avalonia.Controls { _visiblePrimaryCommands.Clear(); _overflowItems.Clear(); + SetOverflowMode(_overflowPrimarySecondarySeparator, false); - foreach (var item in SecondaryCommands) - { - SetOverflowMode(item, true); - _overflowItems.Add(item); - } + var overflowedPrimaryCommands = new List(); var availableWidth = double.IsFinite(_constraintWidth) ? _constraintWidth : Bounds.Width; @@ -669,8 +684,6 @@ namespace Avalonia.Controls ? a.Order.CompareTo(b.Order) : a.Index.CompareTo(b.Index)); - // Separators stay in the primary bar but are not counted toward maxItems. - // If no non-separator buttons fit, separators are moved to overflow too. var visibleIndices = new HashSet(); int nonSeparatorCount = 0; for (var i = 0; i < prioritized.Count; i++) @@ -688,6 +701,8 @@ namespace Avalonia.Controls if (nonSeparatorCount == 0) visibleIndices.Clear(); + TrimOrphanedSeparatorsFromVisibleCommands(PrimaryCommands, visibleIndices); + for (var i = 0; i < PrimaryCommands.Count; i++) { if (visibleIndices.Contains(i)) @@ -695,15 +710,20 @@ namespace Avalonia.Controls SetOverflowMode(PrimaryCommands[i], false); _visiblePrimaryCommands.Add(PrimaryCommands[i]); } + else if (PrimaryCommands[i] is CommandBarSeparator) + { + SetOverflowMode(PrimaryCommands[i], false); + } else { SetOverflowMode(PrimaryCommands[i], true); - _overflowItems.Add(PrimaryCommands[i]); + overflowedPrimaryCommands.Add(PrimaryCommands[i]); } } } } + AddOverflowItems(overflowedPrimaryCommands); HasSecondaryCommands = _overflowItems.Count > 0; UpdateOverflowButtonVisibility(); } @@ -731,6 +751,8 @@ namespace Avalonia.Controls if (SecondaryCommands != null) foreach (var cmd in SecondaryCommands) ApplyLabelPositionToElement(cmd); + + ApplyLabelPositionToElement(_overflowPrimarySecondarySeparator); } private void ApplyLabelPositionToElement(ICommandBarElement element) @@ -758,5 +780,121 @@ namespace Avalonia.Controls _ => HasSecondaryCommands // Auto }; } + + private void AddOverflowItems(IReadOnlyList overflowedPrimaryCommands) + { + for (var i = 0; i < overflowedPrimaryCommands.Count; i++) + _overflowItems.Add(overflowedPrimaryCommands[i]); + + if (overflowedPrimaryCommands.Count > 0 && HasVisibleElements(SecondaryCommands)) + { + SetOverflowMode(_overflowPrimarySecondarySeparator, true); + _overflowItems.Add(_overflowPrimarySecondarySeparator); + } + + foreach (var item in SecondaryCommands) + { + SetOverflowMode(item, true); + _overflowItems.Add(item); + } + } + + private void RebuildSecondaryCommandVisibilitySubscriptions() + { + _secondaryCommandVisibilitySubscriptions.Clear(); + + if (SecondaryCommands is null) + return; + + for (var i = 0; i < SecondaryCommands.Count; i++) + { + if (SecondaryCommands[i] is Avalonia.Visual visual) + { + bool isInitialValue = true; + visual.GetObservable(Avalonia.Visual.IsVisibleProperty) + .Subscribe(_ => + { + if (isInitialValue) + { + isInitialValue = false; + return; + } + + UpdateDynamicOverflow(); + }) + .DisposeWith(_secondaryCommandVisibilitySubscriptions); + } + } + } + + private static bool HasVisibleElements(IList commands) + { + for (var i = 0; i < commands.Count; i++) + { + if (commands[i] is Avalonia.Visual visual && visual.IsVisible) + return true; + } + + return false; + } + + private static void TrimOrphanedSeparatorsFromVisibleCommands( + IList commands, HashSet visibleIndices) + { + var toRemove = new List(); + for (var i = 0; i < commands.Count; i++) + { + if (!visibleIndices.Contains(i) || commands[i] is not CommandBarSeparator) + continue; + + bool hasNonSeparatorBefore = FindNonSeparatorInVisibleCommands( + commands, visibleIndices, forward: false, startIndex: i - 1, out _); + bool hasNonSeparatorAfter = FindNonSeparatorInVisibleCommands( + commands, visibleIndices, forward: true, startIndex: i + 1, out _); + + if (!hasNonSeparatorBefore || !hasNonSeparatorAfter) + toRemove.Add(i); + } + + foreach (var idx in toRemove) + visibleIndices.Remove(idx); + + bool previousWasSeparator = false; + for (var i = 0; i < commands.Count; i++) + { + if (!visibleIndices.Contains(i)) + continue; + + if (commands[i] is CommandBarSeparator) + { + if (previousWasSeparator) + visibleIndices.Remove(i); + else + previousWasSeparator = true; + } + else + { + previousWasSeparator = false; + } + } + } + + private static bool FindNonSeparatorInVisibleCommands( + IList commands, HashSet visibleIndices, + bool forward, int startIndex, out int foundIndex) + { + foundIndex = -1; + var i = startIndex; + while (forward ? i < commands.Count : i >= 0) + { + if (visibleIndices.Contains(i) && commands[i] is not CommandBarSeparator) + { + foundIndex = i; + return true; + } + i += forward ? 1 : -1; + } + return false; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs b/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs index 1950cff16c..371cbe4d6d 100644 --- a/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs @@ -874,13 +874,17 @@ public class CommandBarItemWidthTests : ScopedTestBase public void ItemWidthBottom_Controls_HowManyButtonsFit() { var cb = CreateWithWidth(300); - cb.SecondaryCommands!.Add(new CommandBarButton()); // forces overflow button + var secondary = new CommandBarButton(); + cb.SecondaryCommands!.Add(secondary); // forces overflow button for (int i = 0; i < 4; i++) cb.PrimaryCommands!.Add(new CommandBarButton()); cb.IsDynamicOverflowEnabled = true; Assert.Equal(3, cb.VisiblePrimaryCommands.Count); - Assert.Equal(1, cb.OverflowItems.Count - 1); // -1 for the secondary command + Assert.Equal(3, cb.OverflowItems.Count); + Assert.IsType(cb.OverflowItems[0]); + Assert.IsType(cb.OverflowItems[1]); + Assert.Same(secondary, cb.OverflowItems[2]); } [Fact] @@ -1196,6 +1200,309 @@ public class CommandBarOverflowKeyboardTests : ScopedTestBase } } +public class CommandBarSeparatorOverflowTests : ScopedTestBase +{ + private static CommandBar CreateWithWidth(double width) + { + var cb = new CommandBar(); + cb.Measure(new Size(width, double.PositiveInfinity)); + return cb; + } + + [Fact] + public void TrailingSeparator_IsNotLastVisibleItem() + { + // [Btn, Btn, Sep, Btn] with room for 2 buttons: Sep should NOT trail. + var cb = CreateWithWidth(300); + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.IsNotType(cb.VisiblePrimaryCommands[^1]); + } + + [Fact] + public void TrailingSeparator_MovedToOverflow() + { + // [Btn, Sep, Btn, Btn] with room for 1 button: Sep after the single visible button should overflow. + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 260; + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(1, cb.VisiblePrimaryCommands.Count); + Assert.IsType(cb.VisiblePrimaryCommands[0]); + } + + [Fact] + public void MultipleSeparators_AllTrailingOnesStripped() + { + // [Btn, Sep, Sep, Btn] with room for 1: both trailing separators should be stripped. + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 260; + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(1, cb.VisiblePrimaryCommands.Count); + Assert.IsType(cb.VisiblePrimaryCommands[0]); + } + + [Fact] + public void MidSeparator_StaysVisible_WhenButtonsOnBothSides() + { + // [Btn, Sep, Btn] with room for all: separator stays. + var cb = CreateWithWidth(300); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(3, cb.VisiblePrimaryCommands.Count); + Assert.IsType(cb.VisiblePrimaryCommands[1]); + } + + [Fact] + public void AllButtonsOverflow_SeparatorsAlsoOverflow() + { + // [Sep, Btn, Btn] with room for 0: everything overflows. + var cb = CreateWithWidth(50); + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.Empty(cb.VisiblePrimaryCommands); + } + + [Fact] + public void LeadingSeparator_IsStrippedFromVisible() + { + // [Sep, Btn, Btn, Btn] with room for 2: leading Sep should be stripped. + var cb = CreateWithWidth(300); + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.IsNotType(cb.VisiblePrimaryCommands[0]); + } + + [Fact] + public void ConsecutiveSeparators_CollapsedToOne() + { + // [Btn, Sep, Sep, Btn] all fit: only one separator should remain. + var cb = CreateWithWidth(300); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + int sepCount = CountSeparators(cb.VisiblePrimaryCommands); + Assert.Equal(1, sepCount); + } + + [Fact] + public void OrphanedMidSeparator_RemovedWhenNeighborOverflows() + { + // [Btn1, Sep, Btn2, Sep, Btn3] with room for 2: Btn3 overflows, + // second Sep becomes trailing and is removed. First Sep stays. + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 100; + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.IsNotType(cb.VisiblePrimaryCommands[^1]); + Assert.Equal(1, CountSeparators(cb.VisiblePrimaryCommands)); + } + + [Fact] + public void SeparatorBetweenOverflowedButtons_IsRemoved() + { + // [Btn1, Btn2, Sep, Btn3, Btn4] with room for 2: Btn3 and Btn4 overflow, + // Sep has no non-separator after it in visible set, so it is removed. + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 100; + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(0, CountSeparators(cb.VisiblePrimaryCommands)); + } + + [Fact] + public void MultipleSeparatorGroups_OnlyValidOnesRemain() + { + // [Btn, Sep, Btn, Sep, Btn, Sep, Btn] with room for 3: + // last Btn overflows, last Sep becomes trailing, the rest stay. + var cb = CreateWithWidth(300); + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarButton()); + cb.IsDynamicOverflowEnabled = true; + + Assert.IsNotType(cb.VisiblePrimaryCommands[^1]); + Assert.IsNotType(cb.VisiblePrimaryCommands[0]); + } + + [Fact] + public void OnlySeparators_AllOverflow() + { + // [Sep, Sep, Sep] with no buttons: all should overflow. + var cb = CreateWithWidth(300); + cb.SecondaryCommands!.Add(new CommandBarButton()); + cb.PrimaryCommands!.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.PrimaryCommands.Add(new CommandBarSeparator()); + cb.IsDynamicOverflowEnabled = true; + + Assert.Empty(cb.VisiblePrimaryCommands); + } + + [Fact] + public void PrimarySeparator_IsRemovedInsteadOfBecomingFirstOverflowItem() + { + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 260; + + var leadingSeparator = new CommandBarSeparator(); + var firstButton = new CommandBarButton(); + var overflowedButton = new CommandBarButton(); + + cb.PrimaryCommands!.Add(leadingSeparator); + cb.PrimaryCommands.Add(firstButton); + cb.PrimaryCommands.Add(overflowedButton); + cb.IsDynamicOverflowEnabled = true; + + Assert.Single(cb.OverflowItems); + Assert.Same(overflowedButton, cb.OverflowItems[0]); + Assert.DoesNotContain(leadingSeparator, cb.OverflowItems); + } + + [Fact] + public void OverflowedPrimaryCommands_PrecedeSecondaryCommands_WithSyntheticSeparator() + { + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 260; + + var visiblePrimary = new CommandBarButton(); + var originalPrimarySeparator = new CommandBarSeparator(); + var overflowedPrimaryOne = new CommandBarButton(); + var overflowedPrimaryTwo = new CommandBarButton(); + var secondary = new CommandBarButton(); + + cb.PrimaryCommands!.Add(visiblePrimary); + cb.PrimaryCommands.Add(originalPrimarySeparator); + cb.PrimaryCommands.Add(overflowedPrimaryOne); + cb.PrimaryCommands.Add(overflowedPrimaryTwo); + cb.SecondaryCommands!.Add(secondary); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(4, cb.OverflowItems.Count); + Assert.Same(overflowedPrimaryOne, cb.OverflowItems[0]); + Assert.Same(overflowedPrimaryTwo, cb.OverflowItems[1]); + Assert.IsType(cb.OverflowItems[2]); + Assert.NotSame(originalPrimarySeparator, cb.OverflowItems[2]); + Assert.Same(secondary, cb.OverflowItems[3]); + Assert.DoesNotContain(originalPrimarySeparator, cb.OverflowItems); + } + + [Fact] + public void HiddenSecondaryCommands_DoNotGetSyntheticOverflowSeparator() + { + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 260; + + var visiblePrimary = new CommandBarButton(); + var overflowedPrimary = new CommandBarButton(); + var hiddenSecondary = new CommandBarButton { IsVisible = false }; + + cb.PrimaryCommands!.Add(visiblePrimary); + cb.PrimaryCommands.Add(overflowedPrimary); + cb.SecondaryCommands!.Add(hiddenSecondary); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(2, cb.OverflowItems.Count); + Assert.Same(overflowedPrimary, cb.OverflowItems[0]); + Assert.Same(hiddenSecondary, cb.OverflowItems[1]); + Assert.DoesNotContain(cb.OverflowItems, x => x is CommandBarSeparator); + } + + [Fact] + public void TogglingSecondaryVisibility_RebuildsSyntheticOverflowSeparator() + { + var cb = CreateWithWidth(300); + cb.ItemWidthBottom = 260; + + var visiblePrimary = new CommandBarButton(); + var overflowedPrimary = new CommandBarButton(); + var secondary = new CommandBarButton(); + + cb.PrimaryCommands!.Add(visiblePrimary); + cb.PrimaryCommands.Add(overflowedPrimary); + cb.SecondaryCommands!.Add(secondary); + cb.IsDynamicOverflowEnabled = true; + + Assert.Equal(3, cb.OverflowItems.Count); + Assert.Same(overflowedPrimary, cb.OverflowItems[0]); + Assert.IsType(cb.OverflowItems[1]); + Assert.Same(secondary, cb.OverflowItems[2]); + + secondary.IsVisible = false; + + Assert.Equal(2, cb.OverflowItems.Count); + Assert.Same(overflowedPrimary, cb.OverflowItems[0]); + Assert.Same(secondary, cb.OverflowItems[1]); + Assert.DoesNotContain(cb.OverflowItems, x => x is CommandBarSeparator); + + secondary.IsVisible = true; + + Assert.Equal(3, cb.OverflowItems.Count); + Assert.Same(overflowedPrimary, cb.OverflowItems[0]); + Assert.IsType(cb.OverflowItems[1]); + Assert.Same(secondary, cb.OverflowItems[2]); + } + + private static int CountSeparators(IReadOnlyList items) + { + int count = 0; + for (var i = 0; i < items.Count; i++) + { + if (items[i] is CommandBarSeparator) + count++; + } + return count; + } +} + file sealed class DelegateCommand : System.Windows.Input.ICommand { private readonly System.Action _execute;