Skip to main content

ferroui_controls/command_bar/
command_bar.rs

1use super::{
2    CommandBarButton, CommandBarDefaultLabelPosition, CommandBarOverflowButtonVisibility, CommandBarSeparator,
3    CommandBarToggleButton, ICommandBarElement,
4};
5use crate::items_source::{IItemsList, ItemsChangedEventArgs, ItemsChangedHandler, ItemsSource, ItemsView};
6use crate::primitives::{
7    Popup, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt,
8};
9use crate::{Button, ContentControl, Control, ControlImpl, ItemsControl};
10use ferroui_base::collections::{
11    CollectionChangedHandler, FerroList, NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs,
12};
13use ferroui_base::input::{
14    FocusChangedEventArgs, InputElement, InputElementImpl, Key, KeyEventArgs, KeyModifiers, NavigationMethod,
15};
16use ferroui_base::interactivity::{
17    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
18};
19use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
20use ferroui_base::reactive::{CompositeDisposable, IDisposable, ObservableExt};
21use ferroui_base::threading::{Dispatcher, DispatcherPriority};
22use ferroui_base::utilities::HandlerList;
23use ferroui_base::{
24    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate, BoxedValue,
25    DirectProperty, FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
26    FerroPropertyChangedEventArgs, Ref, Size, StyledElementImpl, StyledProperty, Visual, VisualImpl, VisualImplExt,
27    VisualTreeAttachmentEventArgs,
28};
29use std::any::Any;
30use std::cell::{Cell, RefCell};
31use std::rc::Rc;
32
33/// A list of command bar elements: the type of the `PrimaryCommands` and
34/// `SecondaryCommands` collections.
35pub type CommandBarElementList = FerroList<Rc<dyn ICommandBarElement>>;
36
37type ElementChangedHandler = CollectionChangedHandler<Rc<dyn ICommandBarElement>>;
38
39/// The read-only, notifying collection of command bar elements that a
40/// [`CommandBar`] exposes as `VisiblePrimaryCommands` and `OverflowItems`
41/// (the read-only observable collection of the reference).
42///
43/// The collection is a reference object: the value is a shared handle,
44/// clones refer to the same collection and handles compare by identity.
45/// Only the command bar that owns it changes it. As an items source its
46/// elements that are controls are items boxed as controls, so that an
47/// items control presents them as their own containers.
48#[derive(Clone)]
49pub struct CommandBarElementCollection(Rc<CommandBarElementCollectionData>);
50
51struct CommandBarElementCollectionData {
52    items: RefCell<Rc<Vec<Rc<dyn ICommandBarElement>>>>,
53    collection_changed: HandlerList<ElementChangedHandler>,
54}
55
56impl PartialEq for CommandBarElementCollection {
57    fn eq(&self, other: &Self) -> bool {
58        Rc::ptr_eq(&self.0, &other.0)
59    }
60}
61
62impl std::fmt::Debug for CommandBarElementCollection {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(f, "CommandBarElementCollection(count = {})", self.count())
65    }
66}
67
68impl CommandBarElementCollection {
69    fn new() -> Self {
70        Self(Rc::new(CommandBarElementCollectionData {
71            items: RefCell::new(Rc::new(Vec::new())),
72            collection_changed: HandlerList::new(),
73        }))
74    }
75
76    /// The number of elements.
77    pub fn count(&self) -> usize {
78        self.0.items.borrow().len()
79    }
80
81    /// Whether the collection has no elements.
82    pub fn is_empty(&self) -> bool {
83        self.count() == 0
84    }
85
86    /// The element at `index`. Panics if out of range.
87    pub fn get(&self, index: usize) -> Rc<dyn ICommandBarElement> {
88        self.0.items.borrow()[index].clone()
89    }
90
91    /// The elements as they are now. The snapshot shares storage with the
92    /// collection until the collection changes.
93    pub fn snapshot(&self) -> Rc<Vec<Rc<dyn ICommandBarElement>>> {
94        self.0.items.borrow().clone()
95    }
96
97    /// A copy of the elements.
98    pub fn to_vec(&self) -> Vec<Rc<dyn ICommandBarElement>> {
99        self.snapshot().to_vec()
100    }
101
102    /// Whether the collection contains `element`.
103    pub fn contains(&self, element: &Rc<dyn ICommandBarElement>) -> bool {
104        self.index_of(element).is_some()
105    }
106
107    /// The index of `element`, if the collection contains it.
108    pub fn index_of(&self, element: &Rc<dyn ICommandBarElement>) -> Option<usize> {
109        self.0.items.borrow().iter().position(|item| item == element)
110    }
111
112    /// Subscribes to the changes of the collection. Returns the token for
113    /// [`remove_collection_changed`](Self::remove_collection_changed).
114    pub fn add_collection_changed(&self, handler: Rc<ElementChangedHandler>) -> u64 {
115        self.0.collection_changed.add(handler)
116    }
117
118    /// Unsubscribes a handler.
119    pub fn remove_collection_changed(&self, token: u64) -> bool {
120        self.0.collection_changed.remove(token)
121    }
122
123    /// The collection as the source of the items of an items control.
124    pub fn as_items_source(&self) -> ItemsSource {
125        let list: Rc<dyn IItemsList> = self.0.clone();
126        ItemsSource::new(list)
127    }
128
129    /// Removes every element. Notifies of a reset even when the collection
130    /// is empty, as the observable collection of the reference does.
131    fn clear(&self) {
132        *self.0.items.borrow_mut() = Rc::new(Vec::new());
133        self.0.notify(&NotifyCollectionChangedEventArgs {
134            action: NotifyCollectionChangedAction::Reset,
135            new_items: &[],
136            old_items: &[],
137            new_starting_index: -1,
138            old_starting_index: -1,
139        });
140    }
141
142    fn add(&self, element: Rc<dyn ICommandBarElement>) {
143        let index = {
144            let mut items = self.0.items.borrow_mut();
145            let items = Rc::make_mut(&mut items);
146            items.push(element.clone());
147            items.len() - 1
148        };
149        self.0.notify(&NotifyCollectionChangedEventArgs {
150            action: NotifyCollectionChangedAction::Add,
151            new_items: std::slice::from_ref(&element),
152            old_items: &[],
153            new_starting_index: index as i32,
154            old_starting_index: -1,
155        });
156    }
157}
158
159impl CommandBarElementCollectionData {
160    fn notify(&self, e: &NotifyCollectionChangedEventArgs<'_, Rc<dyn ICommandBarElement>>) {
161        if self.collection_changed.is_empty() {
162            return;
163        }
164        for (_, handler) in self.collection_changed.snapshot().iter() {
165            handler(e);
166        }
167    }
168}
169
170/// The item an items control is given for `element`: the control itself
171/// (boxed as a control) when the element is one.
172fn element_item(element: &Rc<dyn ICommandBarElement>) -> Option<BoxedValue> {
173    match element.as_object().and_then(|object| object.downcast_ref::<Control>()) {
174        Some(control) => Some(Control::boxed(control.to_ref())),
175        None => Some(Rc::new(element.clone())),
176    }
177}
178
179impl IItemsList for CommandBarElementCollectionData {
180    fn count(&self) -> usize {
181        self.items.borrow().len()
182    }
183
184    fn get_at(&self, index: usize) -> Option<BoxedValue> {
185        let element = self.items.borrow()[index].clone();
186        element_item(&element)
187    }
188
189    fn is_notifying(&self) -> bool {
190        true
191    }
192
193    fn add_collection_changed(&self, handler: Rc<ItemsChangedHandler>) -> Option<u64> {
194        Some(self.collection_changed.add(Rc::new(
195            move |e: &NotifyCollectionChangedEventArgs<'_, Rc<dyn ICommandBarElement>>| {
196                let new_items: Vec<Option<BoxedValue>> = e.new_items.iter().map(element_item).collect();
197                let old_items: Vec<Option<BoxedValue>> = e.old_items.iter().map(element_item).collect();
198                handler(&ItemsChangedEventArgs {
199                    action: e.action,
200                    new_items: ItemsView::from_slice(&new_items),
201                    old_items: ItemsView::from_slice(&old_items),
202                    new_starting_index: e.new_starting_index,
203                    old_starting_index: e.old_starting_index,
204                });
205            },
206        )))
207    }
208
209    fn remove_collection_changed(&self, token: u64) {
210        self.collection_changed.remove(token);
211    }
212
213    fn as_any(&self) -> &dyn Any {
214        self
215    }
216}
217
218impl From<CommandBarElementCollection> for ItemsSource {
219    fn from(value: CommandBarElementCollection) -> Self {
220        value.as_items_source()
221    }
222}
223
224/// Resets a flag when dropped.
225struct Reset<'a>(&'a Cell<bool>);
226
227impl Drop for Reset<'_> {
228    fn drop(&mut self) {
229        self.0.set(false);
230    }
231}
232
233/// The object behind `element` viewed as class `T`, if it is one.
234fn element_as<T: ferroui_base::ObjectType>(element: &dyn ICommandBarElement) -> Option<&T> {
235    element.as_object().and_then(|object| object.downcast_ref::<T>())
236}
237
238fn is_separator(element: &dyn ICommandBarElement) -> bool {
239    element_as::<CommandBarSeparator>(element).is_some()
240}
241
242/// A command bar that provides primary commands displayed inline and
243/// secondary commands accessible via an overflow menu.
244#[repr(C)]
245pub struct CommandBar {
246    base: TemplatedControl,
247    has_secondary_commands: Cell<bool>,
248    is_overflow_button_visible: Cell<bool>,
249
250    overflow_button: RefCell<Option<Ref<Button>>>,
251    overflow_popup: RefCell<Option<Ref<Popup>>>,
252    overflow_presenter: RefCell<Option<Ref<ItemsControl>>>,
253    content_presenter: RefCell<Option<Ref<Control>>>,
254    // The handlers added to the template parts: click, got focus, key down
255    // and pointer pressed of the overflow button; key down of the overflow
256    // presenter; opened of the overflow popup.
257    overflow_button_handlers: Cell<
258        Option<(RoutedEventHandlerToken, RoutedEventHandlerToken, RoutedEventHandlerToken, RoutedEventHandlerToken)>,
259    >,
260    overflow_presenter_handler: Cell<Option<RoutedEventHandlerToken>>,
261    overflow_popup_opened: RefCell<Option<Rc<dyn IDisposable>>>,
262    // The subscriptions to the changes of the current command lists.
263    primary_commands_changed: Cell<Option<u64>>,
264    secondary_commands_changed: Cell<Option<u64>>,
265
266    visible_primary_commands: CommandBarElementCollection,
267    overflow_items: CommandBarElementCollection,
268    overflow_primary_secondary_separator: Rc<dyn ICommandBarElement>,
269    secondary_command_visibility_subscriptions: CompositeDisposable,
270    is_dynamic_update_in_progress: Cell<bool>,
271    is_opening_overflow_popup: Cell<bool>,
272    has_deferred_dynamic_overflow_update: Cell<bool>,
273    constraint_width: Cell<f64>,
274    opened_via_keyboard: Cell<bool>,
275}
276
277ferro_class!(CommandBar: TemplatedControl);
278
279ferro_class_info!(CommandBar {
280    new: CommandBar::new,
281    markup: {
282        content: PrimaryCommands,
283        properties: [
284            // The lists of commands exist as soon as they are asked for (the getters of
285            // the managed original create them), so markup adds children to them.
286            PrimaryCommands: CommandBarElementList {
287                get: CommandBar::primary_commands,
288                set: |bar: &Ref<CommandBar>, value: CommandBarElementList| bar.set_primary_commands(Some(value))
289            },
290            SecondaryCommands: CommandBarElementList {
291                get: CommandBar::secondary_commands,
292                set: |bar: &Ref<CommandBar>, value: CommandBarElementList| bar.set_secondary_commands(Some(value))
293            },
294            VisiblePrimaryCommands: CommandBarElementCollection { get: CommandBar::visible_primary_commands },
295            OverflowItems: CommandBarElementCollection { get: CommandBar::overflow_items },
296        ],
297        attributes: [
298            TemplatePart("PART_OverflowButton", type(Ref<Button>)),
299            TemplatePart("PART_OverflowPopup", type(Ref<Popup>)),
300            TemplatePart("PART_OverflowPresenter", type(Ref<ItemsControl>)),
301            TemplatePart("PART_ContentPresenter", type(Ref<Control>)),
302        ],
303    },
304});
305
306ferro_impl_classes!(CommandBar: StyledElementImpl, InteractiveImpl, InputElementImpl, ControlImpl);
307
308impl FerroObjectImpl for CommandBar {
309    fn constructed(this: &Self) {
310        Self::parent_constructed(this);
311
312        let primary_commands = CommandBarElementList::new();
313        this.set_current_value(Self::primary_commands_property(), Some(primary_commands));
314
315        let secondary_commands = CommandBarElementList::new();
316        this.set_current_value(Self::secondary_commands_property(), Some(secondary_commands));
317
318        this.rebuild_secondary_command_visibility_subscriptions();
319
320        let weak = this.to_ref().downgrade();
321        this.size_changed(move |_, _| {
322            if let Some(this) = weak.upgrade() {
323                this.command_bar_size_changed();
324            }
325        });
326    }
327
328    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
329        Self::parent_on_property_changed(this, change);
330
331        let property = change.property();
332
333        if property == Self::is_open_property().as_property() {
334            let is_open = change.get_new_value::<bool>();
335            if is_open {
336                this.raise_event(&RoutedEventArgs::with_event(Self::opening_event()));
337                let overflow_popup = this.overflow_popup.borrow().clone();
338                if let Some(overflow_popup) = overflow_popup {
339                    {
340                        this.is_opening_overflow_popup.set(true);
341                        let _reset = Reset(&this.is_opening_overflow_popup);
342                        overflow_popup.set_is_open(true);
343                    }
344
345                    this.apply_deferred_dynamic_overflow_update();
346                }
347                this.raise_event(&RoutedEventArgs::with_event(Self::opened_event()));
348            } else {
349                this.raise_event(&RoutedEventArgs::with_event(Self::closing_event()));
350                let overflow_popup = this.overflow_popup.borrow().clone();
351                if let Some(overflow_popup) = overflow_popup {
352                    overflow_popup.set_is_open(false);
353                }
354                this.raise_event(&RoutedEventArgs::with_event(Self::closed_event()));
355            }
356        } else if property == Self::default_label_position_property().as_property() {
357            this.apply_label_position_to_children();
358            if this.is_dynamic_overflow_enabled() {
359                this.request_dynamic_overflow_update();
360            }
361        } else if property == Self::overflow_button_visibility_property().as_property() {
362            this.update_overflow_button_visibility();
363        } else if property == Self::is_sticky_property().as_property() {
364            this.update_sticky_behavior();
365        } else if property == Self::is_dynamic_overflow_enabled_property().as_property() {
366            this.request_dynamic_overflow_update();
367        } else if property == Self::primary_commands_property().as_property() {
368            let (old_primary, new_primary) = change.get_old_and_new_value::<Option<CommandBarElementList>>();
369            if let (Some(old_primary), Some(token)) = (old_primary, this.primary_commands_changed.take()) {
370                old_primary.remove_collection_changed(token);
371            }
372            if let Some(new_primary) = new_primary {
373                let weak = this.to_ref().downgrade();
374                let token = new_primary.add_collection_changed(Rc::new(
375                    move |e: &NotifyCollectionChangedEventArgs<'_, Rc<dyn ICommandBarElement>>| {
376                        if let Some(this) = weak.upgrade() {
377                            this.on_primary_commands_changed(e);
378                        }
379                    },
380                ));
381                this.primary_commands_changed.set(Some(token));
382            }
383            this.apply_label_position_to_children();
384            this.request_dynamic_overflow_update();
385        } else if property == Self::secondary_commands_property().as_property() {
386            let (old_secondary, new_secondary) = change.get_old_and_new_value::<Option<CommandBarElementList>>();
387            if let (Some(old_secondary), Some(token)) = (old_secondary, this.secondary_commands_changed.take()) {
388                old_secondary.remove_collection_changed(token);
389            }
390            if let Some(new_secondary) = new_secondary {
391                let weak = this.to_ref().downgrade();
392                let token = new_secondary.add_collection_changed(Rc::new(
393                    move |_: &NotifyCollectionChangedEventArgs<'_, Rc<dyn ICommandBarElement>>| {
394                        if let Some(this) = weak.upgrade() {
395                            this.on_secondary_commands_changed();
396                        }
397                    },
398                ));
399                this.secondary_commands_changed.set(Some(token));
400            }
401            this.rebuild_secondary_command_visibility_subscriptions();
402            this.request_dynamic_overflow_update();
403        }
404    }
405}
406
407impl VisualImpl for CommandBar {
408    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
409        Self::parent_on_attached_to_visual_tree(this, e);
410        this.rebuild_secondary_command_visibility_subscriptions();
411    }
412
413    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
414        this.secondary_command_visibility_subscriptions.clear();
415        Self::parent_on_detached_from_visual_tree(this, e);
416    }
417}
418
419impl LayoutableImpl for CommandBar {
420    fn measure_override(this: &Self, available_size: Size) -> Size {
421        let new_constraint = if available_size.width.is_finite() { available_size.width } else { f64::INFINITY };
422
423        if new_constraint != this.constraint_width.get() {
424            this.constraint_width.set(new_constraint);
425            this.request_dynamic_overflow_update();
426        }
427
428        Self::parent_measure_override(this, available_size)
429    }
430}
431
432impl TemplatedControlImpl for CommandBar {
433    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
434        Self::parent_on_apply_template(this, e);
435
436        let overflow_button = this.overflow_button.borrow().clone();
437        if let (Some(overflow_button), Some((click, got_focus, key_down, pointer_pressed))) =
438            (overflow_button, this.overflow_button_handlers.take())
439        {
440            overflow_button.remove_handler(Button::click_event(), click);
441            overflow_button.remove_handler(InputElement::got_focus_event(), got_focus);
442            overflow_button.remove_handler(InputElement::key_down_event(), key_down);
443            overflow_button.remove_handler(InputElement::pointer_pressed_event(), pointer_pressed);
444        }
445        let overflow_presenter = this.overflow_presenter.borrow().clone();
446        if let (Some(overflow_presenter), Some(key_down)) =
447            (overflow_presenter, this.overflow_presenter_handler.take())
448        {
449            overflow_presenter.remove_handler(InputElement::key_down_event(), key_down);
450        }
451        let overflow_popup_opened = this.overflow_popup_opened.take();
452        if let Some(overflow_popup_opened) = overflow_popup_opened {
453            overflow_popup_opened.dispose();
454        }
455
456        let overflow_button = e.name_scope().find_as::<Button>("PART_OverflowButton");
457        let overflow_popup = e.name_scope().find_as::<Popup>("PART_OverflowPopup");
458        let overflow_presenter = e.name_scope().find_as::<ItemsControl>("PART_OverflowPresenter");
459        let content_presenter = e.name_scope().find_as::<Control>("PART_ContentPresenter");
460        *this.overflow_button.borrow_mut() = overflow_button.clone();
461        *this.overflow_popup.borrow_mut() = overflow_popup.clone();
462        *this.overflow_presenter.borrow_mut() = overflow_presenter.clone();
463        *this.content_presenter.borrow_mut() = content_presenter;
464
465        let weak = this.to_ref().downgrade();
466
467        if let Some(overflow_button) = overflow_button {
468            let click = overflow_button.click({
469                let weak = weak.clone();
470                move |_, _| {
471                    if let Some(this) = weak.upgrade() {
472                        this.on_overflow_button_click();
473                    }
474                }
475            });
476            let got_focus = overflow_button.add_handler(InputElement::got_focus_event(), {
477                let weak = weak.clone();
478                move |_: &Interactive, e: &FocusChangedEventArgs| {
479                    if let Some(this) = weak.upgrade() {
480                        this.on_overflow_button_got_focus(e);
481                    }
482                }
483            });
484            let key_down = overflow_button.add_handler_with(
485                InputElement::key_down_event(),
486                {
487                    let weak = weak.clone();
488                    move |_: &Interactive, e: &KeyEventArgs| {
489                        if let Some(this) = weak.upgrade() {
490                            this.on_overflow_button_key_down(e);
491                        }
492                    }
493                },
494                Interactive::DEFAULT_ROUTES,
495                true,
496            );
497            let pointer_pressed = overflow_button.add_handler_with(
498                InputElement::pointer_pressed_event(),
499                {
500                    let weak = weak.clone();
501                    move |_: &Interactive, _| {
502                        if let Some(this) = weak.upgrade() {
503                            this.on_overflow_button_pointer_pressed();
504                        }
505                    }
506                },
507                Interactive::DEFAULT_ROUTES,
508                true,
509            );
510            this.overflow_button_handlers.set(Some((click, got_focus, key_down, pointer_pressed)));
511        }
512        if let Some(overflow_presenter) = overflow_presenter {
513            let key_down = overflow_presenter.add_handler(InputElement::key_down_event(), {
514                let weak = weak.clone();
515                move |_: &Interactive, e: &KeyEventArgs| {
516                    if let Some(this) = weak.upgrade() {
517                        this.on_overflow_presenter_key_down(e);
518                    }
519                }
520            });
521            this.overflow_presenter_handler.set(Some(key_down));
522        }
523        if let Some(overflow_popup) = overflow_popup {
524            let opened = overflow_popup.opened(move || {
525                if let Some(this) = weak.upgrade() {
526                    this.on_overflow_popup_opened();
527                }
528            });
529            *this.overflow_popup_opened.borrow_mut() = Some(opened);
530        }
531
532        this.apply_label_position_to_children();
533        this.update_overflow_button_visibility();
534        this.update_sticky_behavior();
535        this.request_dynamic_overflow_update();
536    }
537}
538
539ferro_properties! {
540    impl CommandBar {
541        /// Defines the `PrimaryCommands` property.
542        pub fn primary_commands_property() -> StyledProperty<Option<CommandBarElementList>> {
543            FerroProperty::register::<CommandBar, _>("PrimaryCommands", None)
544        }
545
546        /// Defines the `SecondaryCommands` property.
547        pub fn secondary_commands_property() -> StyledProperty<Option<CommandBarElementList>> {
548            FerroProperty::register::<CommandBar, _>("SecondaryCommands", None)
549        }
550
551        /// Defines the `Content` property.
552        pub fn content_property() -> StyledProperty<Option<BoxedValue>> {
553            ContentControl::content_property().add_owner::<CommandBar>()
554        }
555
556        /// Defines the `DefaultLabelPosition` property.
557        pub fn default_label_position_property() -> StyledProperty<CommandBarDefaultLabelPosition> {
558            FerroProperty::register::<CommandBar, _>("DefaultLabelPosition", CommandBarDefaultLabelPosition::Bottom)
559        }
560
561        /// Defines the `IsDynamicOverflowEnabled` property.
562        pub fn is_dynamic_overflow_enabled_property() -> StyledProperty<bool> {
563            FerroProperty::register::<CommandBar, _>("IsDynamicOverflowEnabled", false)
564        }
565
566        /// Defines the `OverflowButtonVisibility` property.
567        pub fn overflow_button_visibility_property() -> StyledProperty<CommandBarOverflowButtonVisibility> {
568            FerroProperty::register::<CommandBar, _>(
569                "OverflowButtonVisibility",
570                CommandBarOverflowButtonVisibility::Auto,
571            )
572        }
573
574        /// Defines the `IsOpen` property.
575        pub fn is_open_property() -> StyledProperty<bool> {
576            FerroProperty::register::<CommandBar, _>("IsOpen", false)
577        }
578
579        /// Defines the `IsSticky` property.
580        pub fn is_sticky_property() -> StyledProperty<bool> {
581            FerroProperty::register::<CommandBar, _>("IsSticky", false)
582        }
583
584        /// Defines the `ItemWidthBottom` property.
585        pub fn item_width_bottom_property() -> StyledProperty<f64> {
586            FerroProperty::register::<CommandBar, _>("ItemWidthBottom", 70.0)
587        }
588
589        /// Defines the `ItemWidthRight` property.
590        pub fn item_width_right_property() -> StyledProperty<f64> {
591            FerroProperty::register::<CommandBar, _>("ItemWidthRight", 102.0)
592        }
593
594        /// Defines the `ItemWidthCollapsed` property.
595        pub fn item_width_collapsed_property() -> StyledProperty<f64> {
596            FerroProperty::register::<CommandBar, _>("ItemWidthCollapsed", 42.0)
597        }
598
599        /// Defines the `HasSecondaryCommands` property.
600        pub fn has_secondary_commands_property() -> DirectProperty<CommandBar, bool> {
601            FerroProperty::register_direct::<CommandBar, _>(
602                "HasSecondaryCommands",
603                |o| o.has_secondary_commands(),
604                None,
605                false,
606            )
607        }
608
609        /// Defines the `IsOverflowButtonVisible` property.
610        pub fn is_overflow_button_visible_property() -> DirectProperty<CommandBar, bool> {
611            FerroProperty::register_direct::<CommandBar, _>(
612                "IsOverflowButtonVisible",
613                |o| o.is_overflow_button_visible(),
614                None,
615                false,
616            )
617        }
618    }
619}
620
621impl CommandBar {
622    ferro_routed_event!(
623        /// Defines the `Opening` routed event.
624        pub fn opening_event() -> RoutedEvent<RoutedEventArgs> {
625            RoutedEvent::register::<CommandBar, _>("Opening", RoutingStrategies::BUBBLE)
626        }
627    );
628
629    ferro_routed_event!(
630        /// Defines the `Opened` routed event.
631        pub fn opened_event() -> RoutedEvent<RoutedEventArgs> {
632            RoutedEvent::register::<CommandBar, _>("Opened", RoutingStrategies::BUBBLE)
633        }
634    );
635
636    ferro_routed_event!(
637        /// Defines the `Closing` routed event.
638        pub fn closing_event() -> RoutedEvent<RoutedEventArgs> {
639            RoutedEvent::register::<CommandBar, _>("Closing", RoutingStrategies::BUBBLE)
640        }
641    );
642
643    ferro_routed_event!(
644        /// Defines the `Closed` routed event.
645        pub fn closed_event() -> RoutedEvent<RoutedEventArgs> {
646            RoutedEvent::register::<CommandBar, _>("Closed", RoutingStrategies::BUBBLE)
647        }
648    );
649
650    fn static_constructor() {
651        // A binding that delivers one of the read-only collections to an
652        // items source property (the bindings of the control themes).
653        ItemsSource::register_binding_conversion::<CommandBarElementCollection>();
654    }
655
656    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
657    pub fn construct() -> Self {
658        Self {
659            base: TemplatedControl::construct(),
660            has_secondary_commands: Cell::new(false),
661            is_overflow_button_visible: Cell::new(false),
662            overflow_button: RefCell::new(None),
663            overflow_popup: RefCell::new(None),
664            overflow_presenter: RefCell::new(None),
665            content_presenter: RefCell::new(None),
666            overflow_button_handlers: Cell::new(None),
667            overflow_presenter_handler: Cell::new(None),
668            overflow_popup_opened: RefCell::new(None),
669            primary_commands_changed: Cell::new(None),
670            secondary_commands_changed: Cell::new(None),
671            visible_primary_commands: CommandBarElementCollection::new(),
672            overflow_items: CommandBarElementCollection::new(),
673            overflow_primary_secondary_separator: CommandBarSeparator::new().as_command_bar_element(),
674            secondary_command_visibility_subscriptions: CompositeDisposable::new(),
675            is_dynamic_update_in_progress: Cell::new(false),
676            is_opening_overflow_popup: Cell::new(false),
677            has_deferred_dynamic_overflow_update: Cell::new(false),
678            constraint_width: Cell::new(f64::INFINITY),
679            opened_via_keyboard: Cell::new(false),
680        }
681    }
682
683    /// Creates a command bar.
684    pub fn new() -> Ref<Self> {
685        instantiate(Self::construct())
686    }
687
688    /// Gets the read-only collection of primary commands currently visible
689    /// in the bar (may be a subset when dynamic overflow is active).
690    pub fn visible_primary_commands(&self) -> CommandBarElementCollection {
691        self.visible_primary_commands.clone()
692    }
693
694    /// Gets the read-only collection of items shown in the overflow popup
695    /// (secondary commands plus any primary commands moved to overflow by
696    /// dynamic overflow).
697    pub fn overflow_items(&self) -> CommandBarElementCollection {
698        self.overflow_items.clone()
699    }
700
701    /// Gets the collection of primary commands displayed in the bar. A bar
702    /// without a collection is given a new one.
703    pub fn primary_commands(&self) -> CommandBarElementList {
704        match self.get_value(Self::primary_commands_property()) {
705            Some(value) => value,
706            None => {
707                let list = CommandBarElementList::new();
708                self.set_current_value(Self::primary_commands_property(), Some(list.clone()));
709                list
710            }
711        }
712    }
713
714    /// Sets the collection of primary commands displayed in the bar.
715    pub fn set_primary_commands(&self, value: Option<CommandBarElementList>) {
716        self.set_value(Self::primary_commands_property(), value)
717    }
718
719    /// Gets the collection of secondary commands shown in the overflow
720    /// menu. A bar without a collection is given a new one.
721    pub fn secondary_commands(&self) -> CommandBarElementList {
722        match self.get_value(Self::secondary_commands_property()) {
723            Some(value) => value,
724            None => {
725                let list = CommandBarElementList::new();
726                self.set_current_value(Self::secondary_commands_property(), Some(list.clone()));
727                list
728            }
729        }
730    }
731
732    /// Sets the collection of secondary commands shown in the overflow
733    /// menu.
734    pub fn set_secondary_commands(&self, value: Option<CommandBarElementList>) {
735        self.set_value(Self::secondary_commands_property(), value)
736    }
737
738    /// Gets the custom content displayed at the start (left) of the bar.
739    pub fn content(&self) -> Option<BoxedValue> {
740        self.get_value(Self::content_property())
741    }
742
743    /// Sets the custom content displayed at the start (left) of the bar.
744    pub fn set_content(&self, value: Option<BoxedValue>) {
745        self.set_value(Self::content_property(), value)
746    }
747
748    /// Gets how labels are positioned on command buttons.
749    pub fn default_label_position(&self) -> CommandBarDefaultLabelPosition {
750        self.get_value(Self::default_label_position_property())
751    }
752
753    /// Sets how labels are positioned on command buttons.
754    pub fn set_default_label_position(&self, value: CommandBarDefaultLabelPosition) {
755        self.set_value(Self::default_label_position_property(), value)
756    }
757
758    /// Gets whether primary commands are automatically moved to the
759    /// overflow menu when there is not enough space to display them.
760    pub fn is_dynamic_overflow_enabled(&self) -> bool {
761        self.get_value(Self::is_dynamic_overflow_enabled_property())
762    }
763
764    /// Sets whether primary commands are automatically moved to the
765    /// overflow menu when there is not enough space to display them.
766    pub fn set_is_dynamic_overflow_enabled(&self, value: bool) {
767        self.set_value(Self::is_dynamic_overflow_enabled_property(), value)
768    }
769
770    /// Gets the visibility of the overflow button.
771    pub fn overflow_button_visibility(&self) -> CommandBarOverflowButtonVisibility {
772        self.get_value(Self::overflow_button_visibility_property())
773    }
774
775    /// Sets the visibility of the overflow button.
776    pub fn set_overflow_button_visibility(&self, value: CommandBarOverflowButtonVisibility) {
777        self.set_value(Self::overflow_button_visibility_property(), value)
778    }
779
780    /// Gets whether the overflow menu is open.
781    pub fn is_open(&self) -> bool {
782        self.get_value(Self::is_open_property())
783    }
784
785    /// Sets whether the overflow menu is open.
786    pub fn set_is_open(&self, value: bool) {
787        self.set_value(Self::is_open_property(), value)
788    }
789
790    /// Gets whether the overflow menu stays open after a command is invoked
791    /// (disables light-dismiss behavior).
792    pub fn is_sticky(&self) -> bool {
793        self.get_value(Self::is_sticky_property())
794    }
795
796    /// Sets whether the overflow menu stays open after a command is invoked
797    /// (disables light-dismiss behavior).
798    pub fn set_is_sticky(&self, value: bool) {
799        self.set_value(Self::is_sticky_property(), value)
800    }
801
802    /// Gets the estimated item width used in dynamic-overflow calculations
803    /// when the default label position is
804    /// [`CommandBarDefaultLabelPosition::Bottom`].
805    pub fn item_width_bottom(&self) -> f64 {
806        self.get_value(Self::item_width_bottom_property())
807    }
808
809    /// Sets the estimated item width used when the labels are at the
810    /// bottom.
811    pub fn set_item_width_bottom(&self, value: f64) {
812        self.set_value(Self::item_width_bottom_property(), value)
813    }
814
815    /// Gets the estimated item width used in dynamic-overflow calculations
816    /// when the default label position is
817    /// [`CommandBarDefaultLabelPosition::Right`].
818    pub fn item_width_right(&self) -> f64 {
819        self.get_value(Self::item_width_right_property())
820    }
821
822    /// Sets the estimated item width used when the labels are on the right.
823    pub fn set_item_width_right(&self, value: f64) {
824        self.set_value(Self::item_width_right_property(), value)
825    }
826
827    /// Gets the estimated item width used in dynamic-overflow calculations
828    /// when the default label position is
829    /// [`CommandBarDefaultLabelPosition::Collapsed`].
830    pub fn item_width_collapsed(&self) -> f64 {
831        self.get_value(Self::item_width_collapsed_property())
832    }
833
834    /// Sets the estimated item width used when the labels are collapsed.
835    pub fn set_item_width_collapsed(&self, value: f64) {
836        self.set_value(Self::item_width_collapsed_property(), value)
837    }
838
839    /// Gets whether there are any commands (secondary or overflowed
840    /// primary) in the overflow menu.
841    pub fn has_secondary_commands(&self) -> bool {
842        self.has_secondary_commands.get()
843    }
844
845    fn set_has_secondary_commands(&self, value: bool) {
846        self.set_and_raise_cell(Self::has_secondary_commands_property(), &self.has_secondary_commands, value);
847    }
848
849    /// Gets whether the overflow button is currently visible.
850    pub fn is_overflow_button_visible(&self) -> bool {
851        self.is_overflow_button_visible.get()
852    }
853
854    fn set_is_overflow_button_visible(&self, value: bool) {
855        self.set_and_raise_cell(Self::is_overflow_button_visible_property(), &self.is_overflow_button_visible, value);
856    }
857
858    /// Occurs when the overflow menu is about to open.
859    pub fn opening(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
860        self.add_handler(Self::opening_event(), handler)
861    }
862
863    /// Occurs when the overflow menu has opened.
864    pub fn opened(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
865        self.add_handler(Self::opened_event(), handler)
866    }
867
868    /// Occurs when the overflow menu is about to close.
869    pub fn closing(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
870        self.add_handler(Self::closing_event(), handler)
871    }
872
873    /// Occurs when the overflow menu has closed.
874    pub fn closed(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
875        self.add_handler(Self::closed_event(), handler)
876    }
877
878    fn command_bar_size_changed(&self) {
879        self.request_dynamic_overflow_update();
880    }
881
882    fn on_overflow_button_click(&self) {
883        self.set_current_value(Self::is_open_property(), !self.is_open());
884    }
885
886    fn on_overflow_button_got_focus(&self, e: &FocusChangedEventArgs) {
887        self.opened_via_keyboard
888            .set(matches!(e.navigation_method, NavigationMethod::Directional | NavigationMethod::Tab));
889    }
890
891    fn on_overflow_button_key_down(&self, e: &KeyEventArgs) {
892        if matches!(e.key, Key::Enter | Key::Space) {
893            self.opened_via_keyboard.set(true);
894        }
895    }
896
897    fn on_overflow_button_pointer_pressed(&self) {
898        self.opened_via_keyboard.set(false);
899    }
900
901    fn on_overflow_presenter_key_down(&self, e: &KeyEventArgs) {
902        match e.key {
903            Key::Up => {
904                self.navigate_overflow(false);
905                e.set_handled(true);
906            }
907            Key::Down => {
908                self.navigate_overflow(true);
909                e.set_handled(true);
910            }
911            Key::Home => {
912                self.focus_overflow_item(true, NavigationMethod::Directional);
913                e.set_handled(true);
914            }
915            Key::End => {
916                self.focus_overflow_item(false, NavigationMethod::Directional);
917                e.set_handled(true);
918            }
919            Key::Escape => {
920                self.set_current_value(Self::is_open_property(), false);
921                let overflow_button = self.overflow_button.borrow().clone();
922                if let Some(overflow_button) = overflow_button {
923                    overflow_button.focus_with(NavigationMethod::Unspecified, KeyModifiers::NONE);
924                }
925                e.set_handled(true);
926            }
927            _ => {}
928        }
929    }
930
931    fn on_overflow_popup_opened(&self) {
932        let method =
933            if self.opened_via_keyboard.get() { NavigationMethod::Directional } else { NavigationMethod::Pointer };
934
935        let weak = self.to_ref().downgrade();
936        Dispatcher::ui_thread().post_local(
937            move || {
938                if let Some(this) = weak.upgrade() {
939                    if this.is_open() {
940                        this.focus_overflow_item(true, method);
941                    }
942                }
943            },
944            DispatcherPriority::LOADED,
945        );
946    }
947
948    fn navigate_overflow(&self, forward: bool) {
949        let items = self.get_focusable_overflow_items();
950        if items.is_empty() {
951            return;
952        }
953
954        let current = items.iter().position(|item| item.is_focused() || item.is_keyboard_focus_within());
955
956        let next = match current {
957            None => {
958                if forward {
959                    0
960                } else {
961                    items.len() - 1
962                }
963            }
964            Some(current) => {
965                if forward {
966                    (current + 1) % items.len()
967                } else {
968                    (current + items.len() - 1) % items.len()
969                }
970            }
971        };
972
973        items[next].focus_with(NavigationMethod::Directional, KeyModifiers::NONE);
974    }
975
976    fn focus_overflow_item(&self, first: bool, method: NavigationMethod) {
977        let items = self.get_focusable_overflow_items();
978        if !items.is_empty() {
979            items[if first { 0 } else { items.len() - 1 }].focus_with(method, KeyModifiers::NONE);
980        }
981    }
982
983    fn get_focusable_overflow_items(&self) -> Vec<Ref<Control>> {
984        let mut result = Vec::new();
985        for item in self.overflow_items.snapshot().iter() {
986            if let Some(control) = element_as::<Control>(&**item) {
987                if control.is_enabled() && control.is_visible() && control.focusable() && !is_separator(&**item) {
988                    result.push(control.to_ref());
989                }
990            }
991        }
992        result
993    }
994
995    fn on_primary_commands_changed(&self, e: &NotifyCollectionChangedEventArgs<'_, Rc<dyn ICommandBarElement>>) {
996        for element in e.new_items {
997            self.apply_label_position_to_element(&**element);
998        }
999        self.request_dynamic_overflow_update();
1000    }
1001
1002    fn on_secondary_commands_changed(&self) {
1003        self.rebuild_secondary_command_visibility_subscriptions();
1004        self.request_dynamic_overflow_update();
1005    }
1006
1007    fn request_dynamic_overflow_update(&self) {
1008        if self.is_opening_overflow_popup.get() {
1009            self.has_deferred_dynamic_overflow_update.set(true);
1010            return;
1011        }
1012
1013        self.update_dynamic_overflow();
1014    }
1015
1016    fn apply_deferred_dynamic_overflow_update(&self) {
1017        if !self.has_deferred_dynamic_overflow_update.get() {
1018            return;
1019        }
1020
1021        self.has_deferred_dynamic_overflow_update.set(false);
1022        self.update_dynamic_overflow();
1023    }
1024
1025    fn update_dynamic_overflow(&self) {
1026        // Reading the collections gives a bar without one a new one, which
1027        // runs this method from the change of the property, as in the
1028        // reference.
1029        let primary_commands = self.primary_commands().snapshot();
1030        let secondary_commands = self.secondary_commands().snapshot();
1031        if self.is_dynamic_update_in_progress.get() {
1032            return;
1033        }
1034        self.is_dynamic_update_in_progress.set(true);
1035        let _reset = Reset(&self.is_dynamic_update_in_progress);
1036
1037        self.visible_primary_commands.clear();
1038        self.overflow_items.clear();
1039        self.set_overflow_mode(&*self.overflow_primary_secondary_separator, false);
1040
1041        let mut overflowed_primary_commands: Vec<Rc<dyn ICommandBarElement>> = Vec::new();
1042
1043        let constraint_width = self.constraint_width.get();
1044        let available_width = if constraint_width.is_finite() { constraint_width } else { self.bounds().width };
1045
1046        if !self.is_dynamic_overflow_enabled() || available_width <= 0.0 {
1047            for item in primary_commands.iter() {
1048                self.set_overflow_mode(&**item, false);
1049                self.visible_primary_commands.add(item.clone());
1050            }
1051        } else {
1052            let content_presenter = self.content_presenter.borrow().clone();
1053            let content_width = content_presenter.map_or(0.0, |presenter| presenter.desired_size().width);
1054
1055            if available_width < 50.0 {
1056                for item in primary_commands.iter() {
1057                    self.set_overflow_mode(&**item, false);
1058                    self.visible_primary_commands.add(item.clone());
1059                }
1060            } else {
1061                let item_width = match self.default_label_position() {
1062                    CommandBarDefaultLabelPosition::Right => self.item_width_right(),
1063                    CommandBarDefaultLabelPosition::Collapsed => self.item_width_collapsed(),
1064                    CommandBarDefaultLabelPosition::Bottom => self.item_width_bottom(),
1065                };
1066
1067                let primary_non_sep_count = primary_commands.iter().filter(|item| !is_separator(&***item)).count();
1068
1069                const OVERFLOW_BUTTON_WIDTH: f64 = 48.0;
1070                let padding = self.padding();
1071                let padding_h = padding.left + padding.right;
1072                let base_available = available_width - content_width - padding_h;
1073                let all_fit_without_button = secondary_commands.is_empty()
1074                    && base_available + 2.0 >= primary_non_sep_count as f64 * item_width;
1075
1076                let max_items = if all_fit_without_button {
1077                    primary_non_sep_count
1078                } else {
1079                    let available_for_items = base_available - OVERFLOW_BUTTON_WIDTH;
1080                    let mut max_items = ((available_for_items / item_width) as i32).max(0) as usize;
1081                    if max_items == 0 && !primary_commands.is_empty() && available_for_items > 32.0 {
1082                        max_items = 1;
1083                    }
1084                    max_items
1085                };
1086
1087                // Lower DynamicOverflowOrder = higher priority (stays visible longer).
1088                let mut prioritized: Vec<(usize, i32)> = primary_commands
1089                    .iter()
1090                    .enumerate()
1091                    .map(|(index, item)| (index, Self::get_dynamic_overflow_order(&**item)))
1092                    .collect();
1093
1094                prioritized.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
1095
1096                // The set of the visible indices: one flag for each command.
1097                let mut visible_indices = vec![false; primary_commands.len()];
1098                let mut non_separator_count = 0;
1099                for &(index, _) in &prioritized {
1100                    if is_separator(&*primary_commands[index]) {
1101                        visible_indices[index] = true;
1102                    } else if non_separator_count < max_items {
1103                        visible_indices[index] = true;
1104                        non_separator_count += 1;
1105                    }
1106                }
1107
1108                if non_separator_count == 0 {
1109                    visible_indices.fill(false);
1110                }
1111
1112                Self::trim_orphaned_separators_from_visible_commands(&primary_commands, &mut visible_indices);
1113
1114                for (index, item) in primary_commands.iter().enumerate() {
1115                    if visible_indices[index] {
1116                        self.set_overflow_mode(&**item, false);
1117                        self.visible_primary_commands.add(item.clone());
1118                    } else if is_separator(&**item) {
1119                        self.set_overflow_mode(&**item, false);
1120                    } else {
1121                        self.set_overflow_mode(&**item, true);
1122                        overflowed_primary_commands.push(item.clone());
1123                    }
1124                }
1125            }
1126        }
1127
1128        self.add_overflow_items(&overflowed_primary_commands, &secondary_commands);
1129        self.set_has_secondary_commands(!self.overflow_items.is_empty());
1130        self.update_overflow_button_visibility();
1131    }
1132
1133    fn set_overflow_mode(&self, element: &dyn ICommandBarElement, in_overflow: bool) {
1134        element.set_is_in_overflow(in_overflow);
1135        self.apply_label_position_to_element(element);
1136    }
1137
1138    fn get_dynamic_overflow_order(element: &dyn ICommandBarElement) -> i32 {
1139        if let Some(button) = element_as::<CommandBarButton>(element) {
1140            button.dynamic_overflow_order()
1141        } else if let Some(toggle_button) = element_as::<CommandBarToggleButton>(element) {
1142            toggle_button.dynamic_overflow_order()
1143        } else {
1144            0
1145        }
1146    }
1147
1148    fn apply_label_position_to_children(&self) {
1149        for command in self.primary_commands().snapshot().iter() {
1150            self.apply_label_position_to_element(&**command);
1151        }
1152        for command in self.secondary_commands().snapshot().iter() {
1153            self.apply_label_position_to_element(&**command);
1154        }
1155
1156        self.apply_label_position_to_element(&*self.overflow_primary_secondary_separator);
1157    }
1158
1159    fn apply_label_position_to_element(&self, element: &dyn ICommandBarElement) {
1160        let label_position = if element.is_in_overflow() {
1161            CommandBarDefaultLabelPosition::Right
1162        } else {
1163            self.default_label_position()
1164        };
1165
1166        element.set_is_compact(label_position == CommandBarDefaultLabelPosition::Collapsed);
1167
1168        if let Some(button) = element_as::<CommandBarButton>(element) {
1169            button.set_label_position(label_position);
1170        } else if let Some(toggle_button) = element_as::<CommandBarToggleButton>(element) {
1171            toggle_button.set_label_position(label_position);
1172        }
1173    }
1174
1175    fn update_sticky_behavior(&self) {
1176        let overflow_popup = self.overflow_popup.borrow().clone();
1177        if let Some(overflow_popup) = overflow_popup {
1178            overflow_popup.set_is_light_dismiss_enabled(!self.is_sticky());
1179        }
1180    }
1181
1182    fn update_overflow_button_visibility(&self) {
1183        self.set_is_overflow_button_visible(match self.overflow_button_visibility() {
1184            CommandBarOverflowButtonVisibility::Visible => true,
1185            CommandBarOverflowButtonVisibility::Collapsed => false,
1186            CommandBarOverflowButtonVisibility::Auto => self.has_secondary_commands(),
1187        });
1188    }
1189
1190    fn add_overflow_items(
1191        &self,
1192        overflowed_primary_commands: &[Rc<dyn ICommandBarElement>],
1193        secondary_commands: &[Rc<dyn ICommandBarElement>],
1194    ) {
1195        for item in overflowed_primary_commands {
1196            self.overflow_items.add(item.clone());
1197        }
1198
1199        if !overflowed_primary_commands.is_empty() && Self::has_visible_elements(secondary_commands) {
1200            self.set_overflow_mode(&*self.overflow_primary_secondary_separator, true);
1201            self.overflow_items.add(self.overflow_primary_secondary_separator.clone());
1202        }
1203
1204        for item in secondary_commands {
1205            self.set_overflow_mode(&**item, true);
1206            self.overflow_items.add(item.clone());
1207        }
1208    }
1209
1210    fn rebuild_secondary_command_visibility_subscriptions(&self) {
1211        self.secondary_command_visibility_subscriptions.clear();
1212
1213        for command in self.secondary_commands().snapshot().iter() {
1214            if let Some(visual) = command.as_object().filter(|object| object.is::<Visual>()) {
1215                let is_initial_value = Cell::new(true);
1216                let weak = self.to_ref().downgrade();
1217                let subscription = FerroObjectExtensions::get_observable(visual, Visual::is_visible_property())
1218                    .subscribe_fn(move |_| {
1219                        if is_initial_value.replace(false) {
1220                            return;
1221                        }
1222
1223                        if let Some(this) = weak.upgrade() {
1224                            this.request_dynamic_overflow_update();
1225                        }
1226                    });
1227                self.secondary_command_visibility_subscriptions.add(subscription);
1228            }
1229        }
1230    }
1231
1232    fn has_visible_elements(commands: &[Rc<dyn ICommandBarElement>]) -> bool {
1233        commands.iter().any(|command| element_as::<Visual>(&**command).is_some_and(|visual| visual.is_visible()))
1234    }
1235
1236    fn trim_orphaned_separators_from_visible_commands(
1237        commands: &[Rc<dyn ICommandBarElement>],
1238        visible_indices: &mut [bool],
1239    ) {
1240        let mut to_remove = Vec::new();
1241        for i in 0..commands.len() {
1242            if !visible_indices[i] || !is_separator(&*commands[i]) {
1243                continue;
1244            }
1245
1246            let has_non_separator_before =
1247                Self::find_non_separator_in_visible_commands(commands, visible_indices, false, i as isize - 1)
1248                    .is_some();
1249            let has_non_separator_after =
1250                Self::find_non_separator_in_visible_commands(commands, visible_indices, true, i as isize + 1)
1251                    .is_some();
1252
1253            if !has_non_separator_before || !has_non_separator_after {
1254                to_remove.push(i);
1255            }
1256        }
1257
1258        for index in to_remove {
1259            visible_indices[index] = false;
1260        }
1261
1262        let mut previous_was_separator = false;
1263        for i in 0..commands.len() {
1264            if !visible_indices[i] {
1265                continue;
1266            }
1267
1268            if is_separator(&*commands[i]) {
1269                if previous_was_separator {
1270                    visible_indices[i] = false;
1271                } else {
1272                    previous_was_separator = true;
1273                }
1274            } else {
1275                previous_was_separator = false;
1276            }
1277        }
1278    }
1279
1280    /// The index of the first visible command that is not a separator,
1281    /// searching from `start_index` in the given direction.
1282    fn find_non_separator_in_visible_commands(
1283        commands: &[Rc<dyn ICommandBarElement>],
1284        visible_indices: &[bool],
1285        forward: bool,
1286        start_index: isize,
1287    ) -> Option<usize> {
1288        let mut i = start_index;
1289        while if forward { i < commands.len() as isize } else { i >= 0 } {
1290            let index = i as usize;
1291            if visible_indices[index] && !is_separator(&*commands[index]) {
1292                return Some(index);
1293            }
1294            i += if forward { 1 } else { -1 };
1295        }
1296        None
1297    }
1298}