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#[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 popup_events: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
58 hotkey: Cell<Option<KeyGesture>>,
59 is_embedded_in_menu: Cell<bool>,
60 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 fn on_click(this, e: &RoutedEventArgs);
71 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 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 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 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 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 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 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 }
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
368struct 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
530struct DependencyResolver;
532
533impl DependencyResolver {
534 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 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 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 pub fn hot_key_property() -> StyledProperty<Option<KeyGesture>> {
566 HotKeyManager::hot_key_property().add_owner::<MenuItem>()
567 }
568
569 pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
571 Button::command_parameter_property().add_owner::<MenuItem>()
572 }
573
574 pub fn icon_property() -> StyledProperty<Option<BoxedValue>> {
576 FerroProperty::register::<MenuItem, _>("Icon", None)
577 }
578
579 pub fn input_gesture_property() -> StyledProperty<Option<KeyGesture>> {
581 FerroProperty::register::<MenuItem, _>("InputGesture", None)
582 }
583
584 pub fn is_sub_menu_open_property() -> StyledProperty<bool> {
586 FerroProperty::register::<MenuItem, _>("IsSubMenuOpen", false)
587 }
588
589 pub fn stays_open_on_click_property() -> StyledProperty<bool> {
591 FerroProperty::register::<MenuItem, _>("StaysOpenOnClick", false)
592 }
593
594 pub fn toggle_type_property() -> StyledProperty<MenuItemToggleType> {
596 FerroProperty::register::<MenuItem, _>("ToggleType", MenuItemToggleType::None)
597 }
598
599 pub fn is_checked_property() -> StyledProperty<bool> {
601 FerroProperty::register::<MenuItem, _>("IsChecked", false)
602 }
603
604 pub fn group_name_property() -> StyledProperty<Option<String>> {
606 RadioButton::group_name_property().add_owner::<MenuItem>()
607 }
608 }
609}
610
611impl MenuItem {
612 pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] =
614 &[TemplatePartAttribute::new("PART_Popup", <Popup as StaticType>::TYPE)];
615
616 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 pub fn click_event() -> RoutedEvent<RoutedEventArgs> {
631 RoutedEvent::register::<MenuItem, _>("Click", RoutingStrategies::BUBBLE)
632 }
633 );
634
635 ferro_routed_event!(
636 pub fn pointer_entered_item_event() -> RoutedEvent<RoutedEventArgs> {
638 RoutedEvent::register::<MenuItem, _>("PointerEnteredItem", RoutingStrategies::BUBBLE)
639 }
640 );
641
642 ferro_routed_event!(
643 pub fn pointer_exited_item_event() -> RoutedEvent<RoutedEventArgs> {
645 RoutedEvent::register::<MenuItem, _>("PointerExitedItem", RoutingStrategies::BUBBLE)
646 }
647 );
648
649 ferro_routed_event!(
650 pub fn submenu_opened_event() -> RoutedEvent<RoutedEventArgs> {
652 RoutedEvent::register::<MenuItem, _>("SubmenuOpened", RoutingStrategies::BUBBLE)
653 }
654 );
655
656 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 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 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 pub fn click(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
703 self.add_handler(Self::click_event(), handler)
704 }
705
706 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn has_sub_menu(&self) -> bool {
845 !self.classes().contains(":empty")
846 }
847
848 pub fn is_top_level(&self) -> bool {
851 self.parent().is_some_and(|parent| parent.is::<Menu>())
852 }
853
854 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 pub fn open(&self) {
865 self.set_current_value(Self::is_sub_menu_open_property(), true);
866 }
867
868 pub fn close(&self) {
872 self.set_current_value(Self::is_sub_menu_open_property(), false);
873 }
874
875 pub(crate) fn to_menu_item(&self) -> Rc<dyn IMenuItem> {
877 Rc::new(MenuItemHandle(self.to_ref()))
878 }
879
880 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 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 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 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 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 fn can_execute_changed(&self) {
962 self.try_update_can_execute();
963 }
964
965 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 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 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 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 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 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 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 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 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 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 fn popup_opened(&self) {
1113 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 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}