Skip to main content

ferroui_controls/
menu_item.rs

1use crate::definition_base::{DefinitionBase, SharedSizeScope};
2use crate::generators::RecycleKey;
3use crate::i_clickable_control::register_clickable_control;
4use crate::i_command_source::register_command_source;
5use crate::i_menu::as_menu;
6use crate::i_menu_element::{as_menu_element, IMenuElement};
7use crate::i_menu_item::{as_menu_item, register_menu_item, IMenuItem};
8use crate::i_selectable::{register_selectable, ISelectable};
9use crate::menu_item_access_key_handler::MenuItemAccessKeyHandler;
10use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
11use crate::mixins::{PressedMixin, SelectableMixin};
12use crate::platform::{FeedbackType, IMenuInteractionHandler, PlatformFeedback};
13use crate::primitives::{
14    HeaderedSelectingItemsControl, HeaderedSelectingItemsControlImpl, Popup, SelectingItemsControl,
15    SelectingItemsControlImpl, TemplateAppliedEventArgs, TemplatedControlImpl,
16};
17use crate::radio_button_group_manager::{register_radio_button, IRadioButton};
18use crate::templates::{FuncTemplate, ITemplateOf};
19use crate::{
20    Button, Control, ControlImpl, HotKeyManager, ItemsControl, ItemsControlImpl, Menu, MenuBase, MenuItemToggleType,
21    Panel, RadioButton, StackPanel,
22};
23use ferroui_base::data::{BindingError, BindingPriority, BindingValueType};
24use ferroui_base::input::{
25    AccessKeyHandler, AccessKeyPressedEventArgs, FocusChangedEventArgs, IAccessKeyHandler, IClickableControl,
26    ICommand, ICommandSource, InputElement, InputElementImpl, InputElementImplExt, KeyEventArgs, KeyGesture, NavigationDirection,
27    PointerEventArgs, PointerReleasedEventArgs,
28};
29use ferroui_base::interactivity::{
30    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
31};
32use ferroui_base::layout::LayoutableImpl;
33use ferroui_base::logical_tree::LogicalTreeAttachmentEventArgs;
34use ferroui_base::utilities::WeakEvents;
35use ferroui_base::reactive::{Disposable, IDisposable, IObserver, LightweightSubject, ObservableExt};
36use ferroui_base::{
37    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate, BoxedValue,
38    FerroLocator, FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
39    FerroPropertyChangedEventArgs, IFerroDependencyResolver, Ref, ServiceHandle, StaticType, StyledElement,
40    StyledElementImpl, StyledElementImplExt, StyledProperty, StyledPropertyMetadata, Visual, VisualImpl, VisualImplExt,
41    VisualTreeAttachmentEventArgs,
42};
43use std::any::TypeId;
44use std::cell::{Cell, RefCell};
45use std::rc::Rc;
46
47/// A menu item control.
48#[repr(C)]
49pub struct MenuItem {
50    base: HeaderedSelectingItemsControl,
51    can_execute_change_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
52    is_subscribed_to_command: Cell<bool>,
53    command_can_execute: Cell<bool>,
54    command_binding_error: Cell<bool>,
55    popup: RefCell<Option<Ref<Popup>>>,
56    /// The subscriptions to the `Opened` and `Closed` events of the popup.
57    popup_events: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
58    hotkey: Cell<Option<KeyGesture>>,
59    is_embedded_in_menu: Cell<bool>,
60    /// The shared size scope of the visual parent; see `constructed`.
61    parent_shared_size_scope: LightweightSubject<Option<Rc<SharedSizeScope>>>,
62    parent_shared_size_scope_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
63}
64
65ferro_class! {
66    MenuItem: HeaderedSelectingItemsControl, virtuals MenuItemImpl: HeaderedSelectingItemsControlImpl {
67        /// Invoked when an unhandled `Click` event reaches an element in
68        /// its route that is derived from this class. Implement this method
69        /// to add class handling for this event.
70        fn on_click(this, e: &RoutedEventArgs);
71        /// Invoked when an unhandled `SubmenuOpened` event reaches an
72        /// element in its route that is derived from this class. Implement
73        /// this method to add class handling for this event.
74        fn on_submenu_opened(this, e: &RoutedEventArgs);
75    }
76}
77
78ferro_class_info!(MenuItem { new: MenuItem::new });
79
80ferro_impl_classes!(MenuItem: LayoutableImpl, InteractiveImpl, SelectingItemsControlImpl, HeaderedSelectingItemsControlImpl);
81
82impl ControlImpl for MenuItem {
83    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
84        crate::automation::peers::MenuItemAutomationPeer::new(this).upcast()
85    }
86}
87
88impl FerroObjectImpl for MenuItem {
89    fn constructed(this: &Self) {
90        Self::parent_constructed(this);
91
92        // HACK: This nasty but it's all WPF's fault. Grid uses an inherited
93        // attached property to store SharedSizeGroup state, except property
94        // inheritance is done down the logical tree. In this case, the
95        // control which is setting Grid.IsSharedSizeScope="True" is not in
96        // the logical tree. Instead of fixing the way Grid stores shared
97        // size state, the developers of WPF just created a binding of the
98        // internal state of the visual parent to the menu item. We don't
99        // have much choice but to do the same for now unless we want to
100        // refactor Grid.
101        //
102        // In addition to the hack from WPF, we also make sure to return null
103        // when we have no parent. If we don't do this, inheritance falls
104        // back to the logical tree, causing the shared size scope in the
105        // parent MenuItem to be used, breaking menu layout.
106        let parent_shared_size_scope: Rc<LightweightSubject<Option<Rc<SharedSizeScope>>>> =
107            Rc::new(this.parent_shared_size_scope.clone());
108
109        this.bind(
110            DefinitionBase::private_shared_size_scope_property(),
111            parent_shared_size_scope,
112            BindingPriority::LocalValue,
113        );
114        this.update_parent_shared_size_scope(None);
115    }
116
117    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
118        Self::parent_on_property_changed(this, change);
119
120        let property = change.property();
121
122        if property == Visual::visual_parent_property().as_property() {
123            this.update_parent_shared_size_scope(change.get_new_value::<Option<Ref<Visual>>>());
124        }
125
126        if property == HeaderedSelectingItemsControl::header_property().as_property() {
127            this.header_changed(change);
128        } else if property == Self::icon_property().as_property() {
129            this.icon_changed(change);
130        } else if property == SelectingItemsControl::is_selected_property().as_property() {
131            this.is_selected_changed(change);
132        } else if property == Self::is_sub_menu_open_property().as_property() {
133            this.sub_menu_open_changed(change);
134        } else if property == Self::command_property().as_property() {
135            this.command_changed(change);
136        } else if property == Self::command_parameter_property().as_property() {
137            this.command_parameter_changed(change);
138        } else if property == Self::is_checked_property().as_property() {
139            this.is_checked_changed(change);
140        } else if property == Self::toggle_type_property().as_property() {
141            this.toggle_type_changed(change);
142        } else if property == Self::group_name_property().as_property() {
143            this.group_name_changed(change);
144        } else if property == ItemsControl::item_count_property().as_property() {
145            // A menu item with no sub-menu is effectively disabled if its
146            // command binding failed: this means that the effectively
147            // enabled state depends on whether the number of items in the
148            // menu is 0 or not.
149            let (o, n) = change.get_old_and_new_value::<i32>();
150            if o == 0 || n == 0 {
151                this.update_is_effectively_enabled();
152            }
153        }
154    }
155
156    // TODO: This is confusing for some ppl. Need to think about alternatives here.
157    fn update_data_validation(
158        this: &Self,
159        property: &'static FerroProperty,
160        state: BindingValueType,
161        error: Option<&BindingError>,
162    ) {
163        Self::parent_update_data_validation(this, property, state, error);
164
165        if property == Self::command_property().as_property() {
166            this.command_binding_error.set(state == BindingValueType::BINDING_ERROR);
167            if this.command_binding_error.get() && this.command_can_execute.get() {
168                this.command_can_execute.set(false);
169                this.update_is_effectively_enabled();
170            }
171        }
172    }
173}
174
175impl StyledElementImpl for MenuItem {
176    fn on_attached_to_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
177        // Control attached again, set Hotkey to create a hotkey manager for
178        // this control
179        if let Some(hotkey) = this.hotkey.get() {
180            this.set_current_value(Self::hot_key_property(), Some(hotkey));
181        }
182
183        Self::parent_on_attached_to_logical_tree(this, e);
184
185        let (command, parameter) = (this.command(), this.command_parameter());
186        if let Some(command) = &command {
187            this.subscribe_can_execute_changed(command);
188        }
189
190        this.is_subscribed_to_command.set(true);
191
192        this.try_update_can_execute_with(command.as_ref(), parameter.as_ref());
193
194        let mut parent = this.parent();
195
196        while let Some(current) = parent.clone().filter(|parent| parent.is::<MenuItem>()) {
197            parent = current.parent();
198        }
199
200        let mut is_embedded_in_menu = false;
201        let mut current = parent;
202        while let Some(logical) = current {
203            if as_menu(&logical).is_some() {
204                is_embedded_in_menu = true;
205                break;
206            }
207            current = logical.parent();
208        }
209        this.is_embedded_in_menu.set(is_embedded_in_menu);
210    }
211
212    fn on_detached_from_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
213        // This will cause the hotkey manager to dispose the observer and
214        // the reference to this control
215        if let Some(hotkey) = this.hot_key() {
216            this.hotkey.set(Some(hotkey));
217            this.set_current_value(Self::hot_key_property(), None);
218        }
219
220        Self::parent_on_detached_from_logical_tree(this, e);
221
222        if this.command().is_some() {
223            this.unsubscribe_can_execute_changed();
224        }
225
226        this.is_subscribed_to_command.set(false);
227    }
228}
229
230impl VisualImpl for MenuItem {
231    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
232        Self::parent_on_attached_to_visual_tree(this, e);
233
234        this.try_update_can_execute();
235        this.register_in_menu_interaction_handler();
236    }
237}
238
239impl InputElementImpl for MenuItem {
240    fn is_enabled_core(this: &Self) -> bool {
241        this.is_enabled() && (this.has_sub_menu() || this.command_can_execute.get())
242    }
243
244    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
245        Self::parent_on_pointer_released(this, e);
246
247        if !this.is_embedded_in_menu.get() {
248            // Normally the Menu's IMenuInteractionHandler is sending the
249            // click events for us. However when the item is not embedded
250            // into a menu we need to send them ourselves.
251            this.raise_event(&RoutedEventArgs::with_event(Self::click_event()));
252        }
253    }
254
255    fn on_got_focus(this: &Self, e: &FocusChangedEventArgs) {
256        Self::parent_on_got_focus(this, e);
257
258        let container: Ref<Control> = this.to_ref().upcast();
259        if let Some(owner) = SelectingItemsControl::selecting_items_control_from_item_container(&container) {
260            owner.update_selection_from_event(&container, e);
261        }
262    }
263
264    fn on_key_down(_this: &Self, _e: &KeyEventArgs) {
265        // Don't handle here: let event bubble up to menu.
266    }
267
268    fn on_pointer_entered(this: &Self, e: &PointerEventArgs) {
269        Self::parent_on_pointer_entered(this, e);
270        this.raise_event(&RoutedEventArgs::with_event(Self::pointer_entered_item_event()));
271    }
272
273    fn on_pointer_exited(this: &Self, e: &PointerEventArgs) {
274        Self::parent_on_pointer_exited(this, e);
275        this.raise_event(&RoutedEventArgs::with_event(Self::pointer_exited_item_event()));
276    }
277}
278
279impl TemplatedControlImpl for MenuItem {
280    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
281        let old_popup = this.popup.borrow_mut().take();
282        if let Some(old_popup) = old_popup {
283            let popup_events = this.popup_events.borrow_mut().take();
284            if let Some((opened, closed)) = popup_events {
285                opened.dispose();
286                closed.dispose();
287            }
288            old_popup.set_dependency_resolver(None);
289        }
290
291        let popup = e.name_scope().find_as::<Popup>("PART_Popup");
292        *this.popup.borrow_mut() = popup.clone();
293
294        if let Some(popup) = popup {
295            popup.set_dependency_resolver(Some(DependencyResolver::instance()));
296
297            let weak = this.to_ref().downgrade();
298            let opened = popup.opened({
299                let weak = weak.clone();
300                move || {
301                    if let Some(this) = weak.upgrade() {
302                        this.popup_opened();
303                    }
304                }
305            });
306            let closed = popup.closed(move || {
307                if let Some(this) = weak.upgrade() {
308                    this.popup_closed();
309                }
310            });
311            *this.popup_events.borrow_mut() = Some((opened, closed));
312        }
313    }
314}
315
316impl ItemsControlImpl for MenuItem {
317    fn create_container_for_item_override(
318        _this: &Self,
319        _item: &Option<BoxedValue>,
320        _index: i32,
321        _recycle_key: Option<RecycleKey>,
322    ) -> Ref<Control> {
323        MenuItem::new().upcast()
324    }
325
326    fn needs_container_override(_this: &Self, item: &Option<BoxedValue>, _index: i32) -> (bool, Option<RecycleKey>) {
327        MenuBase::needs_menu_container(item)
328    }
329}
330
331impl MenuItemImpl for MenuItem {
332    fn on_click(this: &Self, e: &RoutedEventArgs) {
333        let (command, parameter) = (this.command(), this.command_parameter());
334        if let Some(command) = command {
335            if !e.handled() && command.can_execute(parameter.as_ref()) {
336                command.execute(parameter.as_ref());
337                e.set_handled(true);
338            }
339        }
340    }
341
342    fn on_submenu_opened(this: &Self, e: &RoutedEventArgs) {
343        let menu_item = e.source().and_then(|source| source.cast::<MenuItem>());
344
345        if let Some(menu_item) = menu_item {
346            let this_element: &StyledElement = this;
347            if menu_item.parent().is_some_and(|parent| std::ptr::eq::<StyledElement>(&*parent, this_element)) {
348                for child in this.to_menu_item().sub_items() {
349                    if child.element() != menu_item.clone().upcast::<InputElement>() && child.is_sub_menu_open() {
350                        child.set_is_sub_menu_open(false);
351                    }
352                }
353            }
354        }
355    }
356}
357
358impl ISelectable for MenuItem {
359    fn is_selected(&self) -> bool {
360        MenuItem::is_selected(self)
361    }
362
363    fn set_is_selected(&self, value: bool) {
364        MenuItem::set_is_selected(self, value)
365    }
366}
367
368/// The menu item viewed through the interfaces it implements.
369struct MenuItemHandle(Ref<MenuItem>);
370
371impl IMenuElement for MenuItemHandle {
372    fn element(&self) -> Ref<InputElement> {
373        self.0.clone().upcast()
374    }
375
376    fn selected_item(&self) -> Option<Rc<dyn IMenuItem>> {
377        let index = self.0.selected_index();
378        if index != -1 {
379            self.0.container_from_index(index).and_then(|container| as_menu_item(&container))
380        } else {
381            None
382        }
383    }
384
385    fn set_selected_item(&self, value: Option<Rc<dyn IMenuItem>>) {
386        let index = match value.and_then(|value| value.element().cast::<Control>()) {
387            Some(c) => self.0.index_from_container(&c),
388            None => -1,
389        };
390        self.0.set_selected_index(index);
391    }
392
393    fn sub_items(&self) -> Vec<Rc<dyn IMenuItem>> {
394        StyledElement::logical_children(&self.0).to_vec().iter().filter_map(|child| as_menu_item(child)).collect()
395    }
396
397    fn open(&self) {
398        self.0.open();
399    }
400
401    fn close(&self) {
402        self.0.close();
403    }
404
405    fn move_selection(&self, direction: NavigationDirection, wrap: bool) -> bool {
406        self.0.move_selection(direction, wrap, false)
407    }
408
409    fn as_menu_item(&self) -> Option<Rc<dyn IMenuItem>> {
410        Some(Rc::new(MenuItemHandle(self.0.clone())))
411    }
412}
413
414impl IMenuItem for MenuItemHandle {
415    fn has_sub_menu(&self) -> bool {
416        self.0.has_sub_menu()
417    }
418
419    fn is_pointer_over_sub_menu(&self) -> bool {
420        self.0.popup.borrow().as_ref().is_some_and(|popup| popup.is_pointer_over_popup())
421    }
422
423    fn is_sub_menu_open(&self) -> bool {
424        self.0.is_sub_menu_open()
425    }
426
427    fn set_is_sub_menu_open(&self, value: bool) {
428        self.0.set_current_value(MenuItem::is_sub_menu_open_property(), value);
429    }
430
431    fn stays_open_on_click(&self) -> bool {
432        self.0.stays_open_on_click()
433    }
434
435    fn set_stays_open_on_click(&self, value: bool) {
436        self.0.set_current_value(MenuItem::stays_open_on_click_property(), value);
437    }
438
439    fn is_top_level(&self) -> bool {
440        self.0.is_top_level()
441    }
442
443    fn parent(&self) -> Option<Rc<dyn IMenuElement>> {
444        self.0.parent().and_then(|parent| as_menu_element(&parent))
445    }
446
447    fn toggle_type(&self) -> MenuItemToggleType {
448        self.0.toggle_type()
449    }
450
451    fn group_name(&self) -> Option<String> {
452        self.0.group_name()
453    }
454
455    fn is_checked(&self) -> bool {
456        self.0.is_checked()
457    }
458
459    fn set_is_checked(&self, value: bool) {
460        self.0.set_current_value(MenuItem::is_checked_property(), value);
461    }
462
463    fn raise_click(&self) {
464        self.0.raise_event(&RoutedEventArgs::with_event(MenuItem::click_event()));
465    }
466}
467
468impl IRadioButton for MenuItemHandle {
469    fn logical(&self) -> Ref<StyledElement> {
470        self.0.clone().upcast()
471    }
472
473    fn group_name(&self) -> Option<String> {
474        self.0.group_name()
475    }
476
477    fn toggle_type(&self) -> MenuItemToggleType {
478        self.0.toggle_type()
479    }
480
481    fn is_checked(&self) -> bool {
482        self.0.is_checked()
483    }
484
485    fn set_is_checked(&self, value: bool) {
486        self.0.set_current_value(MenuItem::is_checked_property(), value);
487    }
488}
489
490impl ICommandSource for MenuItemHandle {
491    fn command(&self) -> Option<Rc<dyn ICommand>> {
492        self.0.command()
493    }
494
495    fn command_parameter(&self) -> Option<BoxedValue> {
496        self.0.command_parameter()
497    }
498
499    fn can_execute_changed(&self) {
500        self.0.can_execute_changed();
501    }
502
503    fn is_effectively_enabled(&self) -> bool {
504        self.0.is_effectively_enabled()
505    }
506}
507
508impl IClickableControl for MenuItemHandle {
509    fn click(&self, handler: Rc<dyn Fn(&RoutedEventArgs)>) -> Rc<dyn IDisposable> {
510        let token = self.0.click(move |_, e| handler(e));
511        let menu_item = self.0.downgrade();
512        Disposable::create(move || {
513            if let Some(menu_item) = menu_item.upgrade() {
514                menu_item.remove_handler(MenuItem::click_event(), token);
515            }
516        })
517    }
518
519    fn raise_click(&self) {
520        if self.0.is_effectively_enabled() {
521            self.0.raise_event(&RoutedEventArgs::with_event(MenuItem::click_event()));
522        }
523    }
524
525    fn is_effectively_enabled(&self) -> bool {
526        self.0.is_effectively_enabled()
527    }
528}
529
530/// A dependency resolver which returns a [`MenuItemAccessKeyHandler`].
531struct DependencyResolver;
532
533impl DependencyResolver {
534    /// Gets the default instance of [`DependencyResolver`].
535    fn instance() -> Rc<dyn IFerroDependencyResolver> {
536        ferroui_base::ferro_thread_local! {
537            static INSTANCE: Rc<DependencyResolver> = Rc::new(DependencyResolver);
538        }
539        INSTANCE.with(|instance| instance.clone())
540    }
541}
542
543impl IFerroDependencyResolver for DependencyResolver {
544    /// Gets a service of the specified type.
545    fn get_service_untyped(&self, service_type: TypeId) -> Option<ServiceHandle> {
546        if service_type == TypeId::of::<dyn IAccessKeyHandler>() {
547            let handler: Rc<dyn IAccessKeyHandler> = MenuItemAccessKeyHandler::new();
548            let handle: ServiceHandle = Rc::new(handler);
549            Some(handle)
550        } else {
551            FerroLocator::current().get_service_untyped(service_type)
552        }
553    }
554}
555
556ferro_properties! {
557    impl MenuItem {
558        /// Defines the `Command` property.
559        pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
560            Button::command_property()
561                .add_owner_with::<MenuItem>(StyledPropertyMetadata::new(None).with_enable_data_validation(true))
562        }
563
564        /// Defines the `HotKey` property.
565        pub fn hot_key_property() -> StyledProperty<Option<KeyGesture>> {
566            HotKeyManager::hot_key_property().add_owner::<MenuItem>()
567        }
568
569        /// Defines the `CommandParameter` property.
570        pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
571            Button::command_parameter_property().add_owner::<MenuItem>()
572        }
573
574        /// Defines the `Icon` property.
575        pub fn icon_property() -> StyledProperty<Option<BoxedValue>> {
576            FerroProperty::register::<MenuItem, _>("Icon", None)
577        }
578
579        /// Defines the `InputGesture` property.
580        pub fn input_gesture_property() -> StyledProperty<Option<KeyGesture>> {
581            FerroProperty::register::<MenuItem, _>("InputGesture", None)
582        }
583
584        /// Defines the `IsSubMenuOpen` property.
585        pub fn is_sub_menu_open_property() -> StyledProperty<bool> {
586            FerroProperty::register::<MenuItem, _>("IsSubMenuOpen", false)
587        }
588
589        /// Defines the `StaysOpenOnClick` property.
590        pub fn stays_open_on_click_property() -> StyledProperty<bool> {
591            FerroProperty::register::<MenuItem, _>("StaysOpenOnClick", false)
592        }
593
594        /// Defines the `ToggleType` property.
595        pub fn toggle_type_property() -> StyledProperty<MenuItemToggleType> {
596            FerroProperty::register::<MenuItem, _>("ToggleType", MenuItemToggleType::None)
597        }
598
599        /// Defines the `IsChecked` property.
600        pub fn is_checked_property() -> StyledProperty<bool> {
601            FerroProperty::register::<MenuItem, _>("IsChecked", false)
602        }
603
604        /// Defines the `GroupName` property.
605        pub fn group_name_property() -> StyledProperty<Option<String>> {
606            RadioButton::group_name_property().add_owner::<MenuItem>()
607        }
608    }
609}
610
611impl MenuItem {
612    /// The named parts expected in the control template.
613    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] =
614        &[TemplatePartAttribute::new("PART_Popup", <Popup as StaticType>::TYPE)];
615
616    /// The pseudoclasses set by the class.
617    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[
618        ":separator",
619        ":radio",
620        ":toggle",
621        ":checked",
622        ":icon",
623        ":open",
624        ":pressed",
625        ":selected",
626    ]);
627
628    ferro_routed_event!(
629        /// Defines the `Click` event.
630        pub fn click_event() -> RoutedEvent<RoutedEventArgs> {
631            RoutedEvent::register::<MenuItem, _>("Click", RoutingStrategies::BUBBLE)
632        }
633    );
634
635    ferro_routed_event!(
636        /// Defines the `PointerEnteredItem` event.
637        pub fn pointer_entered_item_event() -> RoutedEvent<RoutedEventArgs> {
638            RoutedEvent::register::<MenuItem, _>("PointerEnteredItem", RoutingStrategies::BUBBLE)
639        }
640    );
641
642    ferro_routed_event!(
643        /// Defines the `PointerExitedItem` event.
644        pub fn pointer_exited_item_event() -> RoutedEvent<RoutedEventArgs> {
645            RoutedEvent::register::<MenuItem, _>("PointerExitedItem", RoutingStrategies::BUBBLE)
646        }
647    );
648
649    ferro_routed_event!(
650        /// Defines the `SubmenuOpened` event.
651        pub fn submenu_opened_event() -> RoutedEvent<RoutedEventArgs> {
652            RoutedEvent::register::<MenuItem, _>("SubmenuOpened", RoutingStrategies::BUBBLE)
653        }
654    );
655
656    /// Initializes static members of the [`MenuItem`] class.
657    fn static_constructor() {
658        register_menu_item::<MenuItem>(|menu_item| Rc::new(MenuItemHandle(menu_item)));
659        register_radio_button::<MenuItem>(|menu_item| Rc::new(MenuItemHandle(menu_item)));
660        register_command_source::<MenuItem>(|menu_item| Rc::new(MenuItemHandle(menu_item)));
661        register_clickable_control::<MenuItem>(|menu_item| Rc::new(MenuItemHandle(menu_item)));
662        register_selectable::<MenuItem>();
663
664        // The default value for the `ItemsPanel` property.
665        let default_panel: Rc<dyn ITemplateOf<Option<Ref<Panel>>>> =
666            FuncTemplate::new(|| Some(StackPanel::new().upcast::<Panel>()));
667
668        SelectableMixin::attach::<MenuItem>(SelectingItemsControl::is_selected_property());
669        PressedMixin::attach::<MenuItem>();
670        InputElement::focusable_property().override_default_value::<MenuItem>(true);
671        ItemsControl::items_panel_property().override_default_value::<MenuItem>(default_panel);
672        Self::click_event().add_class_handler::<MenuItem>(|x, e| x.on_click(e));
673        Self::submenu_opened_event().add_class_handler::<MenuItem>(|x, e| x.on_submenu_opened(e));
674        crate::automation::AutomationProperties::is_offscreen_behavior_property()
675            .override_default_value::<MenuItem>(crate::automation::IsOffscreenBehavior::FromClip);
676        AccessKeyHandler::access_key_pressed_event().add_class_handler::<MenuItem>(Self::on_access_key_pressed);
677        PlatformFeedback::feedback_type_property().override_default_value::<MenuItem>(FeedbackType::Auto);
678    }
679
680    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
681    pub fn construct() -> Self {
682        Self {
683            base: HeaderedSelectingItemsControl::construct(),
684            can_execute_change_subscription: RefCell::new(None),
685            is_subscribed_to_command: Cell::new(false),
686            command_can_execute: Cell::new(true),
687            command_binding_error: Cell::new(false),
688            popup: RefCell::new(None),
689            popup_events: RefCell::new(None),
690            hotkey: Cell::new(None),
691            is_embedded_in_menu: Cell::new(false),
692            parent_shared_size_scope: LightweightSubject::new(),
693            parent_shared_size_scope_subscription: RefCell::new(None),
694        }
695    }
696
697    pub fn new() -> Ref<Self> {
698        instantiate(Self::construct())
699    }
700
701    /// Occurs when a [`MenuItem`] without a submenu is clicked.
702    pub fn click(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
703        self.add_handler(Self::click_event(), handler)
704    }
705
706    /// Occurs when the pointer enters a menu item.
707    ///
708    /// A bubbling version of the pointer entered event for menu items.
709    pub fn pointer_entered_item(
710        &self,
711        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
712    ) -> RoutedEventHandlerToken {
713        self.add_handler(Self::pointer_entered_item_event(), handler)
714    }
715
716    /// Raised when the pointer leaves a menu item.
717    ///
718    /// A bubbling version of the pointer exited event for menu items.
719    pub fn pointer_exited_item(
720        &self,
721        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
722    ) -> RoutedEventHandlerToken {
723        self.add_handler(Self::pointer_exited_item_event(), handler)
724    }
725
726    /// Occurs when a [`MenuItem`]'s submenu is opened.
727    pub fn submenu_opened(
728        &self,
729        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
730    ) -> RoutedEventHandlerToken {
731        self.add_handler(Self::submenu_opened_event(), handler)
732    }
733
734    /// Gets or sets the command associated with the menu item.
735    pub fn command(&self) -> Option<Rc<dyn ICommand>> {
736        self.get_value(Self::command_property())
737    }
738
739    pub fn set_command(&self, value: Option<Rc<dyn ICommand>>) {
740        self.set_value(Self::command_property(), value)
741    }
742
743    /// Gets or sets a key gesture associated with this control.
744    pub fn hot_key(&self) -> Option<KeyGesture> {
745        self.get_value(Self::hot_key_property())
746    }
747
748    pub fn set_hot_key(&self, value: Option<KeyGesture>) {
749        self.set_value(Self::hot_key_property(), value)
750    }
751
752    /// Gets or sets the parameter to pass to the `Command` property of a
753    /// [`MenuItem`].
754    pub fn command_parameter(&self) -> Option<BoxedValue> {
755        self.get_value(Self::command_parameter_property())
756    }
757
758    pub fn set_command_parameter(&self, value: Option<BoxedValue>) {
759        self.set_value(Self::command_parameter_property(), value)
760    }
761
762    /// Gets or sets the icon that appears in a [`MenuItem`].
763    pub fn icon(&self) -> Option<BoxedValue> {
764        self.get_value(Self::icon_property())
765    }
766
767    pub fn set_icon(&self, value: Option<BoxedValue>) {
768        self.set_value(Self::icon_property(), value)
769    }
770
771    /// Gets or sets the input gesture that will be displayed in the menu
772    /// item.
773    ///
774    /// Setting this property does not cause the input gesture to be handled
775    /// by the menu item, it simply displays the gesture text in the menu.
776    pub fn input_gesture(&self) -> Option<KeyGesture> {
777        self.get_value(Self::input_gesture_property())
778    }
779
780    pub fn set_input_gesture(&self, value: Option<KeyGesture>) {
781        self.set_value(Self::input_gesture_property(), value)
782    }
783
784    /// Gets or sets a value indicating whether the [`MenuItem`] is
785    /// currently selected.
786    pub fn is_selected(&self) -> bool {
787        self.get_value(SelectingItemsControl::is_selected_property())
788    }
789
790    pub fn set_is_selected(&self, value: bool) {
791        self.set_value(SelectingItemsControl::is_selected_property(), value)
792    }
793
794    /// Gets or sets a value that indicates whether the submenu of the
795    /// [`MenuItem`] is open.
796    pub fn is_sub_menu_open(&self) -> bool {
797        self.get_value(Self::is_sub_menu_open_property())
798    }
799
800    pub fn set_is_sub_menu_open(&self, value: bool) {
801        self.set_value(Self::is_sub_menu_open_property(), value)
802    }
803
804    /// Gets or sets a value that indicates the submenu that this
805    /// [`MenuItem`] is within should not close when this item is clicked.
806    pub fn stays_open_on_click(&self) -> bool {
807        self.get_value(Self::stays_open_on_click_property())
808    }
809
810    pub fn set_stays_open_on_click(&self, value: bool) {
811        self.set_value(Self::stays_open_on_click_property(), value)
812    }
813
814    /// Gets or sets the toggle type of the menu item.
815    pub fn toggle_type(&self) -> MenuItemToggleType {
816        self.get_value(Self::toggle_type_property())
817    }
818
819    pub fn set_toggle_type(&self, value: MenuItemToggleType) {
820        self.set_value(Self::toggle_type_property(), value)
821    }
822
823    /// Gets or sets if menu item is checked when the toggle type is
824    /// [`MenuItemToggleType::CheckBox`] or [`MenuItemToggleType::Radio`].
825    pub fn is_checked(&self) -> bool {
826        self.get_value(Self::is_checked_property())
827    }
828
829    pub fn set_is_checked(&self, value: bool) {
830        self.set_value(Self::is_checked_property(), value)
831    }
832
833    /// Gets or sets the menu item group name when the toggle type is
834    /// [`MenuItemToggleType::Radio`].
835    pub fn group_name(&self) -> Option<String> {
836        self.get_value(Self::group_name_property())
837    }
838
839    pub fn set_group_name(&self, value: Option<String>) {
840        self.set_value(Self::group_name_property(), value)
841    }
842
843    /// Gets a value that indicates whether the [`MenuItem`] has a submenu.
844    pub fn has_sub_menu(&self) -> bool {
845        !self.classes().contains(":empty")
846    }
847
848    /// Gets a value that indicates whether the [`MenuItem`] is a top-level
849    /// main menu item.
850    pub fn is_top_level(&self) -> bool {
851        self.parent().is_some_and(|parent| parent.is::<Menu>())
852    }
853
854    /// The interaction handler of the menu the item is within.
855    pub(crate) fn menu_interaction_handler(&self) -> Option<Rc<dyn IMenuInteractionHandler>> {
856        self.find_logical_ancestor_of_type::<MenuBase>(false)
857            .or_else(|| self.find_ancestor_of_type::<MenuBase>(false))
858            .map(|menu| menu.interaction_handler())
859    }
860
861    /// Opens the submenu.
862    ///
863    /// This has the same effect as setting `IsSubMenuOpen` to true.
864    pub fn open(&self) {
865        self.set_current_value(Self::is_sub_menu_open_property(), true);
866    }
867
868    /// Closes the submenu.
869    ///
870    /// This has the same effect as setting `IsSubMenuOpen` to false.
871    pub fn close(&self) {
872        self.set_current_value(Self::is_sub_menu_open_property(), false);
873    }
874
875    /// The menu item viewed as [`IMenuItem`].
876    pub(crate) fn to_menu_item(&self) -> Rc<dyn IMenuItem> {
877        Rc::new(MenuItemHandle(self.to_ref()))
878    }
879
880    /// Feeds the shared size scope of the visual parent to the binding
881    /// created in `constructed`.
882    fn update_parent_shared_size_scope(&self, visual_parent: Option<Ref<Visual>>) {
883        let subscription = self.parent_shared_size_scope_subscription.borrow_mut().take();
884        if let Some(subscription) = subscription {
885            subscription.dispose();
886        }
887
888        match visual_parent.and_then(|parent| parent.cast::<Control>()) {
889            Some(parent) => {
890                let subject = self.parent_shared_size_scope.clone();
891                let parent: &ferroui_base::FerroObject = &parent;
892                let subscription = FerroObjectExtensions::get_observable(
893                    parent,
894                    DefinitionBase::private_shared_size_scope_property(),
895                )
896                    .subscribe_fn(move |scope| subject.on_next(scope));
897                *self.parent_shared_size_scope_subscription.borrow_mut() = Some(subscription);
898            }
899            None => self.parent_shared_size_scope.on_next(None),
900        }
901    }
902
903    /// Closes all submenus of the menu item.
904    fn close_submenus(&self) {
905        for child in self.to_menu_item().sub_items() {
906            child.set_is_sub_menu_open(false);
907        }
908    }
909
910    /// Called when the `Command` property changes.
911    fn command_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
912        let (old_command, new_command) = e.get_old_and_new_value::<Option<Rc<dyn ICommand>>>();
913
914        if self.is_subscribed_to_command.get() {
915            if old_command.is_some() {
916                self.unsubscribe_can_execute_changed();
917            }
918
919            if let Some(new_command) = &new_command {
920                self.subscribe_can_execute_changed(new_command);
921            }
922        }
923        self.try_update_can_execute_with(new_command.as_ref(), self.command_parameter().as_ref());
924    }
925
926    fn on_access_key_pressed(sender: &MenuItem, e: &AccessKeyPressedEventArgs) {
927        if e.handled() || e.target().is_some() {
928            return;
929        }
930
931        e.set_target(Some(sender.to_ref().upcast()));
932        e.set_handled(true);
933    }
934
935    /// Called when the `CommandParameter` property changes.
936    fn command_parameter_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
937        let (command, parameter) = (self.command(), e.get_new_value::<Option<BoxedValue>>());
938        self.try_update_can_execute_with(command.as_ref(), parameter.as_ref());
939    }
940
941    fn subscribe_can_execute_changed(&self, command: &Rc<dyn ICommand>) {
942        // A weak subscription: it ends with this control, so a command
943        // that outlives the control keeps no handler of it.
944        let subscription = WeakEvents::subscribe_command_can_execute_changed(command, &self.to_ref(), |this| {
945            this.can_execute_changed()
946        });
947        let old = self.can_execute_change_subscription.replace(Some(subscription));
948        if let Some(old) = old {
949            old.dispose();
950        }
951    }
952
953    fn unsubscribe_can_execute_changed(&self) {
954        let subscription = self.can_execute_change_subscription.take();
955        if let Some(subscription) = subscription {
956            subscription.dispose();
957        }
958    }
959
960    /// Called when the "can execute changed" event of the command fires.
961    fn can_execute_changed(&self) {
962        self.try_update_can_execute();
963    }
964
965    /// Tries to evaluate CanExecute value of a Command if menu is opened
966    fn try_update_can_execute(&self) {
967        self.try_update_can_execute_with(self.command().as_ref(), self.command_parameter().as_ref());
968    }
969
970    fn try_update_can_execute_with(&self, command: Option<&Rc<dyn ICommand>>, parameter: Option<&BoxedValue>) {
971        let Some(command) = command else {
972            self.command_can_execute.set(!self.command_binding_error.get());
973            self.update_is_effectively_enabled();
974            return;
975        };
976
977        // Perf optimization - only raise CanExecute event if the menu is open
978        if !self.is_attached_to_logical_tree()
979            || self
980                .parent()
981                .and_then(|parent| parent.cast::<MenuItem>())
982                .is_some_and(|parent| !parent.is_sub_menu_open())
983        {
984            return;
985        }
986
987        let can_execute = command.can_execute(parameter);
988        if can_execute != self.command_can_execute.get() {
989            self.command_can_execute.set(can_execute);
990            self.update_is_effectively_enabled();
991        }
992    }
993
994    /// The default menu interaction handler of the menu the item is within,
995    /// if the menu uses it.
996    fn with_default_menu_interaction_handler(&self, f: impl FnOnce(&crate::platform::DefaultMenuInteractionHandler)) {
997        if let Some(handler) = self.menu_interaction_handler() {
998            if let Some(handler) = handler.as_default_menu_interaction_handler() {
999                f(handler);
1000            }
1001        }
1002    }
1003
1004    /// Called when the `GroupName` property changes.
1005    fn group_name_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1006        let old_group_name = e.get_old_value::<Option<String>>().flatten();
1007        self.with_default_menu_interaction_handler(|handler| {
1008            handler.on_group_or_type_changed(&MenuItemHandle(self.to_ref()), old_group_name.as_deref())
1009        });
1010    }
1011
1012    /// Called when the `ToggleType` property changes.
1013    fn toggle_type_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1014        let new_value = e.get_new_value::<MenuItemToggleType>();
1015        self.pseudo_classes().set(":radio", new_value == MenuItemToggleType::Radio);
1016        self.pseudo_classes().set(":toggle", new_value == MenuItemToggleType::CheckBox);
1017
1018        self.with_default_menu_interaction_handler(|handler| {
1019            handler.on_group_or_type_changed(&MenuItemHandle(self.to_ref()), self.group_name().as_deref())
1020        });
1021    }
1022
1023    /// Called when the `IsChecked` property changes.
1024    fn is_checked_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1025        let new_value = e.get_new_value::<bool>();
1026        self.pseudo_classes().set(":checked", new_value);
1027
1028        if new_value {
1029            self.with_default_menu_interaction_handler(|handler| {
1030                handler.on_checked_changed(&MenuItemHandle(self.to_ref()))
1031            });
1032        }
1033    }
1034
1035    fn is_separator_header(value: &Option<BoxedValue>) -> bool {
1036        value.as_ref().is_some_and(|value| {
1037            let value: &dyn ferroui_base::AnyValue = &**value;
1038            value.downcast_ref::<String>().is_some_and(|text| text == "-")
1039                || value.downcast_ref::<&'static str>().is_some_and(|text| *text == "-")
1040        })
1041    }
1042
1043    /// Called when the `Header` property changes.
1044    fn header_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1045        let (old_value, new_value) = e.get_old_and_new_value::<Option<BoxedValue>>();
1046        if Self::is_separator_header(&new_value) {
1047            self.pseudo_classes().add_pseudo(":separator");
1048            self.set_focusable(false);
1049        } else if Self::is_separator_header(&old_value) {
1050            self.pseudo_classes().remove_pseudo(":separator");
1051            self.set_focusable(true);
1052        }
1053    }
1054
1055    /// Called when the `Icon` property changes.
1056    fn icon_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1057        let (old_value, new_value) = e.get_old_and_new_value::<Option<BoxedValue>>();
1058
1059        if let Some(old_value) = &old_value {
1060            if let Some(old_logical) = Control::logical_from_boxed(old_value) {
1061                StyledElement::logical_children(self).remove(&old_logical);
1062            }
1063
1064            self.pseudo_classes().remove_pseudo(":icon");
1065        }
1066
1067        if let Some(new_value) = &new_value {
1068            if let Some(new_logical) = Control::logical_from_boxed(new_value) {
1069                StyledElement::logical_children(self).add(new_logical);
1070            }
1071
1072            self.pseudo_classes().add_pseudo(":icon");
1073        }
1074    }
1075
1076    /// Called when the `IsSelected` property changes.
1077    fn is_selected_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1078        let parent_menu = self.parent().and_then(|parent| parent.cast::<Menu>());
1079
1080        if e.get_new_value::<bool>() && parent_menu.is_none_or(|parent_menu| parent_menu.is_open()) {
1081            self.focus();
1082        }
1083    }
1084
1085    /// Called when the `IsSubMenuOpen` property changes.
1086    fn sub_menu_open_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
1087        let value = e.get_new_value::<bool>();
1088
1089        if value {
1090            let items: Vec<Ref<MenuItem>> = self
1091                .items_view()
1092                .iter()
1093                .filter_map(|item| item.as_ref().and_then(Control::from_boxed))
1094                .filter_map(|control| control.cast::<MenuItem>())
1095                .collect();
1096
1097            for item in items {
1098                item.try_update_can_execute();
1099            }
1100
1101            self.raise_event(&RoutedEventArgs::with_event(Self::submenu_opened_event()));
1102            self.set_current_value(SelectingItemsControl::is_selected_property(), true);
1103            self.pseudo_classes().add_pseudo(":open");
1104        } else {
1105            self.close_submenus();
1106            self.set_selected_index(-1);
1107            self.pseudo_classes().remove_pseudo(":open");
1108        }
1109    }
1110
1111    /// Called when the submenu's popup is opened.
1112    fn popup_opened(&self) {
1113        // If we're using overlay popups, there's a chance we need to do a
1114        // layout pass before the child items are added to the visual tree.
1115        // If we don't do this here, then selection breaks.
1116        if self.presenter().is_some_and(|presenter| !presenter.is_attached_to_visual_tree()) {
1117            self.update_layout();
1118        }
1119
1120        let selected = self.selected_index();
1121
1122        if selected != -1 {
1123            if let Some(container) = self.container_from_index(selected) {
1124                container.focus();
1125            }
1126        }
1127    }
1128
1129    /// Called when the submenu's popup is closed.
1130    fn popup_closed(&self) {
1131        self.set_selected_item(None);
1132    }
1133
1134    fn register_in_menu_interaction_handler(&self) {
1135        if self.toggle_type() != MenuItemToggleType::Radio {
1136            return;
1137        }
1138
1139        self.with_default_menu_interaction_handler(|handler| {
1140            handler.on_group_or_type_changed(&MenuItemHandle(self.to_ref()), None)
1141        });
1142    }
1143}