Skip to main content

ferroui_controls/
context_menu.rs

1use crate::diagnostics::IPopupHostProvider;
2use crate::platform::{DefaultMenuInteractionHandler, FeedbackAction, IMenuInteractionHandler, PlatformFeedbackExtensions};
3use crate::primitives::popup_positioning::{
4    CustomPopupPlacementCallback, PopupAnchor, PopupGravity, PopupPositionerConstraintAdjustment,
5};
6use crate::primitives::{CustomPopupPlacementCallbackValue, IPopupHost, Popup, SelectingItemsControlImpl, TemplatedControlImpl};
7use crate::{Application, Control, ControlImpl, ItemsControlImpl, MenuBase, MenuBaseImpl, MenuItem, PlacementMode};
8use ferroui_base::input::{ContextRequestedEventArgs, FocusManager, InputElement, InputElementImpl, KeyEventArgs};
9use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEventArgs};
10use ferroui_base::layout::LayoutableImpl;
11use ferroui_base::reactive::{Disposable, IDisposable};
12use ferroui_base::styling::{ISetterValue, Setter};
13use ferroui_base::utilities::{CancelEventArgs, HandlerList};
14use ferroui_base::{
15    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, ElementRef, FerroObjectImpl,
16    FerroObjectImplExt, FerroPropertyChangedEventArgs, Nullable, Rect, Ref, StyledElement, StyledElementImpl,
17    StyledProperty, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs, WeakRef,
18};
19use std::cell::RefCell;
20use std::rc::Rc;
21
22/// A control context menu.
23#[repr(C)]
24pub struct ContextMenu {
25    base: MenuBase,
26    popup: RefCell<Option<Ref<Popup>>>,
27    /// The controls the menu is attached to. The controls own the menu (it
28    /// is the value of their `ContextMenu` property), so the references
29    /// back are weak.
30    attached_controls: RefCell<Option<Vec<WeakRef<Control>>>>,
31    /// The element that had the focus when the popup opened. Only written,
32    /// as in the reference; held weakly since it is not owned by the menu.
33    previous_focus: RefCell<Option<WeakRef<InputElement>>>,
34    popup_host_changed_handler: HandlerList<dyn Fn(Option<&Rc<dyn IPopupHost>>)>,
35    opening: HandlerList<dyn Fn(&CancelEventArgs)>,
36    closing: HandlerList<dyn Fn(&CancelEventArgs)>,
37    /// The system backdrop of the menu (winui-materials.md).
38    element_backdrop: crate::media::ElementSystemBackdrop,
39}
40
41ferro_class!(ContextMenu: MenuBase);
42ferro_class_info!(ContextMenu { new: ContextMenu::new });
43
44ferro_impl_classes!(
45    ContextMenu: StyledElementImpl,
46    LayoutableImpl,
47    InteractiveImpl,
48    InputElementImpl,
49    ControlImpl,
50    TemplatedControlImpl,
51    ItemsControlImpl,
52    SelectingItemsControlImpl
53);
54
55impl FerroObjectImpl for ContextMenu {
56    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
57        Self::parent_on_property_changed(this, change);
58
59        if change.property() == Self::window_manager_add_shadow_hint_property().as_property() {
60            let popup = this.popup.borrow().clone();
61            if let Some(popup) = popup {
62                popup.set_window_manager_add_shadow_hint(change.get_new_value::<bool>());
63            }
64        }
65        if change.property() == Self::system_backdrop_property().as_property() && this.is_attached_to_visual_tree() {
66            this.element_backdrop.connect(this, this.system_backdrop());
67        }
68    }
69}
70
71impl VisualImpl for ContextMenu {
72    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
73        Self::parent_on_attached_to_visual_tree(this, e);
74        this.element_backdrop.connect(this, this.system_backdrop());
75    }
76
77    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
78        Self::parent_on_detached_from_visual_tree(this, e);
79        this.element_backdrop.disconnect();
80    }
81}
82
83impl MenuBaseImpl for ContextMenu {
84    /// Opens the menu.
85    fn open(this: &Self) {
86        this.open_at(None);
87    }
88
89    /// Closes the menu.
90    fn close(this: &Self) {
91        if !this.is_open() {
92            return;
93        }
94
95        let popup = this.popup.borrow().clone();
96        if let Some(popup) = popup {
97            if popup.is_visible() {
98                popup.set_is_open(false);
99            }
100        }
101    }
102}
103
104impl ISetterValue for ContextMenu {
105    fn initialize(&self, setter: &Setter) {
106        // ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
107        // the behavior defined in Control which requires controls to be wrapped in a <template>.
108        if setter.property() != Some(Control::context_menu_property().as_property()) {
109            panic!("Cannot use a control as a Setter value. Wrap the control in a <Template>.");
110        }
111    }
112}
113
114ferro_properties! {
115    impl ContextMenu {
116        /// Defines the `HorizontalOffset` property.
117        pub fn horizontal_offset_property() -> StyledProperty<f64> {
118            Popup::horizontal_offset_property().add_owner::<ContextMenu>()
119        }
120
121        /// Defines the `VerticalOffset` property.
122        pub fn vertical_offset_property() -> StyledProperty<f64> {
123            Popup::vertical_offset_property().add_owner::<ContextMenu>()
124        }
125
126        /// Defines the `PlacementAnchor` property.
127        pub fn placement_anchor_property() -> StyledProperty<PopupAnchor> {
128            Popup::placement_anchor_property().add_owner::<ContextMenu>()
129        }
130
131        /// Defines the `PlacementConstraintAdjustment` property.
132        pub fn placement_constraint_adjustment_property() -> StyledProperty<PopupPositionerConstraintAdjustment> {
133            Popup::placement_constraint_adjustment_property().add_owner::<ContextMenu>()
134        }
135
136        /// Defines the `PlacementGravity` property.
137        pub fn placement_gravity_property() -> StyledProperty<PopupGravity> {
138            Popup::placement_gravity_property().add_owner::<ContextMenu>()
139        }
140
141        /// Defines the `Placement` property.
142        pub fn placement_property() -> StyledProperty<PlacementMode> {
143            Popup::placement_property().add_owner::<ContextMenu>()
144        }
145
146        /// Defines the `PlacementRect` property.
147        pub fn placement_rect_property() -> StyledProperty<Option<Rect>> {
148            Popup::placement_rect_property().add_owner::<ContextMenu>()
149        }
150
151        /// Defines the `WindowManagerAddShadowHint` property.
152        pub fn window_manager_add_shadow_hint_property() -> StyledProperty<bool> {
153            Popup::window_manager_add_shadow_hint_property().add_owner::<ContextMenu>()
154        }
155
156        /// Defines the `PlacementTarget` property.
157        ///
158        /// The menu does not own its placement target, so the value is an
159        /// element reference.
160        pub fn placement_target_property() -> StyledProperty<Option<ElementRef<Control>>> {
161            Popup::placement_target_property().add_owner::<ContextMenu>()
162        }
163
164        /// Defines the `CustomPopupPlacementCallback` property.
165        pub fn custom_popup_placement_callback_property() -> StyledProperty<Option<CustomPopupPlacementCallbackValue>> {
166            Popup::custom_popup_placement_callback_property().add_owner::<ContextMenu>()
167        }
168
169        /// Defines the `SystemBackdrop` property: the system backdrop drawn as the
170        /// background of the menu (the property of a menu flyout presenter, a FerroUI
171        /// addition on the context menu, which is a menu flyout presenter of its own).
172        pub fn system_backdrop_property() -> StyledProperty<Option<Ref<crate::media::SystemBackdrop>>> {
173            crate::flyouts::MenuFlyoutPresenter::system_backdrop_property().add_owner::<ContextMenu>()
174        }
175    }
176}
177
178impl ContextMenu {
179    /// Initializes static members of the [`ContextMenu`] class.
180    fn static_constructor() {
181        Self::placement_property().override_default_value::<ContextMenu>(PlacementMode::Pointer);
182        let _ = Control::context_menu_property().changed().subscribe(Self::context_menu_changed);
183        crate::automation::AutomationProperties::accessibility_view_property()
184            .override_default_value::<ContextMenu>(crate::automation::AccessibilityView::Control);
185        crate::automation::AutomationProperties::control_type_override_property()
186            .override_default_value::<ContextMenu>(Some(crate::automation::peers::AutomationControlType::Menu));
187    }
188
189    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
190    pub fn construct() -> Self {
191        Self::construct_with(DefaultMenuInteractionHandler::new(true))
192    }
193
194    /// Creates the class data of a context menu that uses
195    /// `interaction_handler`.
196    pub fn construct_with(interaction_handler: Rc<dyn IMenuInteractionHandler>) -> Self {
197        Self {
198            base: MenuBase::construct_with(interaction_handler),
199            popup: RefCell::new(None),
200            attached_controls: RefCell::new(None),
201            previous_focus: RefCell::new(None),
202            popup_host_changed_handler: HandlerList::new(),
203            opening: HandlerList::new(),
204            closing: HandlerList::new(),
205            element_backdrop: Default::default(),
206        }
207    }
208
209    /// Initializes a new instance of the [`ContextMenu`] class.
210    pub fn new() -> Ref<Self> {
211        instantiate(Self::construct())
212    }
213
214    /// Initializes a new instance of the [`ContextMenu`] class that uses
215    /// `interaction_handler`.
216    pub fn with_interaction_handler(interaction_handler: Rc<dyn IMenuInteractionHandler>) -> Ref<Self> {
217        instantiate(Self::construct_with(interaction_handler))
218    }
219
220    /// `SystemBackdrop`: the system backdrop drawn as the background of the menu.
221    pub fn system_backdrop(&self) -> Option<Ref<crate::media::SystemBackdrop>> {
222        self.get_value(Self::system_backdrop_property())
223    }
224
225    pub fn set_system_backdrop(&self, value: Option<Ref<crate::media::SystemBackdrop>>) {
226        self.set_value(Self::system_backdrop_property(), value)
227    }
228
229    /// The horizontal offset of the menu in relation to its target; see
230    /// [`Popup::horizontal_offset`].
231    pub fn horizontal_offset(&self) -> f64 {
232        self.get_value(Self::horizontal_offset_property())
233    }
234
235    pub fn set_horizontal_offset(&self, value: f64) {
236        self.set_value(Self::horizontal_offset_property(), value)
237    }
238
239    /// The vertical offset of the menu in relation to its target; see
240    /// [`Popup::vertical_offset`].
241    pub fn vertical_offset(&self) -> f64 {
242        self.get_value(Self::vertical_offset_property())
243    }
244
245    pub fn set_vertical_offset(&self, value: f64) {
246        self.set_value(Self::vertical_offset_property(), value)
247    }
248
249    /// See [`Popup::placement_anchor`].
250    pub fn placement_anchor(&self) -> PopupAnchor {
251        self.get_value(Self::placement_anchor_property())
252    }
253
254    pub fn set_placement_anchor(&self, value: PopupAnchor) {
255        self.set_value(Self::placement_anchor_property(), value)
256    }
257
258    /// See [`Popup::placement_constraint_adjustment`].
259    pub fn placement_constraint_adjustment(&self) -> PopupPositionerConstraintAdjustment {
260        self.get_value(Self::placement_constraint_adjustment_property())
261    }
262
263    pub fn set_placement_constraint_adjustment(&self, value: PopupPositionerConstraintAdjustment) {
264        self.set_value(Self::placement_constraint_adjustment_property(), value)
265    }
266
267    /// See [`Popup::placement_gravity`].
268    pub fn placement_gravity(&self) -> PopupGravity {
269        self.get_value(Self::placement_gravity_property())
270    }
271
272    pub fn set_placement_gravity(&self, value: PopupGravity) {
273        self.set_value(Self::placement_gravity_property(), value)
274    }
275
276    /// See [`Popup::placement`].
277    pub fn placement(&self) -> PlacementMode {
278        self.get_value(Self::placement_property())
279    }
280
281    pub fn set_placement(&self, value: PlacementMode) {
282        self.set_value(Self::placement_property(), value)
283    }
284
285    /// See [`Popup::window_manager_add_shadow_hint`].
286    pub fn window_manager_add_shadow_hint(&self) -> bool {
287        self.get_value(Self::window_manager_add_shadow_hint_property())
288    }
289
290    pub fn set_window_manager_add_shadow_hint(&self, value: bool) {
291        self.set_value(Self::window_manager_add_shadow_hint_property(), value)
292    }
293
294    /// See [`Popup::placement_rect`].
295    pub fn placement_rect(&self) -> Option<Rect> {
296        self.get_value(Self::placement_rect_property())
297    }
298
299    pub fn set_placement_rect(&self, value: Option<Rect>) {
300        self.set_value(Self::placement_rect_property(), value)
301    }
302
303    /// See [`Popup::placement_target`].
304    pub fn placement_target(&self) -> Option<Ref<Control>> {
305        ElementRef::resolve(&self.get_value(Self::placement_target_property()))
306    }
307
308    pub fn set_placement_target(&self, value: impl Into<Nullable<Control>>) {
309        self.set_value(Self::placement_target_property(), ElementRef::from_nullable(value.into().0))
310    }
311
312    /// See [`Popup::custom_popup_placement_callback`].
313    pub fn custom_popup_placement_callback(&self) -> Option<CustomPopupPlacementCallback> {
314        self.get_value(Self::custom_popup_placement_callback_property()).map(|value| value.0)
315    }
316
317    pub fn set_custom_popup_placement_callback(&self, value: Option<CustomPopupPlacementCallback>) {
318        self.set_value(
319            Self::custom_popup_placement_callback_property(),
320            value.map(CustomPopupPlacementCallbackValue),
321        )
322    }
323
324    fn subscribe<F: ?Sized + 'static>(
325        &self,
326        select: fn(&ContextMenu) -> &HandlerList<F>,
327        handler: Rc<F>,
328    ) -> Rc<dyn IDisposable> {
329        let token = select(self).add(handler);
330        let weak = self.to_ref().downgrade();
331        Disposable::create(move || {
332            if let Some(this) = weak.upgrade() {
333                select(&this).remove(token);
334            }
335        })
336    }
337
338    /// Occurs when the value of the `IsOpen` property is changing from
339    /// false to true. Disposing the returned handle unsubscribes.
340    pub fn opening(&self, handler: impl Fn(&CancelEventArgs) + 'static) -> Rc<dyn IDisposable> {
341        self.subscribe::<dyn Fn(&CancelEventArgs)>(|menu| &menu.opening, Rc::new(handler))
342    }
343
344    /// Occurs when the value of the `IsOpen` property is changing from
345    /// true to false. Disposing the returned handle unsubscribes.
346    pub fn closing(&self, handler: impl Fn(&CancelEventArgs) + 'static) -> Rc<dyn IDisposable> {
347        self.subscribe::<dyn Fn(&CancelEventArgs)>(|menu| &menu.closing, Rc::new(handler))
348    }
349
350    /// The controls the menu is attached to that are still alive, or
351    /// `None` when the list was never created.
352    fn attached_controls(&self) -> Option<Vec<Ref<Control>>> {
353        self.attached_controls.borrow().as_ref().map(|controls| controls.iter().filter_map(WeakRef::upgrade).collect())
354    }
355
356    /// Removes the first occurrence of `control` from the attached
357    /// controls.
358    fn remove_attached_control(&self, control: &Ref<Control>) {
359        if let Some(controls) = self.attached_controls.borrow_mut().as_mut() {
360            // Controls that are gone are dropped from the list on the way.
361            controls.retain(|c| c.upgrade().is_some());
362            if let Some(index) = controls.iter().position(|c| c.upgrade().as_ref() == Some(control)) {
363                controls.remove(index);
364            }
365        }
366    }
367
368    /// Called when the `ContextMenu` property changes on a control.
369    fn context_menu_changed(e: &FerroPropertyChangedEventArgs<'_>) {
370        let control = e.sender().downcast_ref::<Control>().expect("the sender is a control");
371        let (old_value, new_value) = e.get_old_and_new_value::<Option<Ref<ContextMenu>>>();
372
373        if let Some(old_menu) = old_value {
374            if let Some((context_requested, context_canceled, attached, detached)) =
375                control.context_menu_handlers().take()
376            {
377                control.remove_handler(InputElement::context_requested_event(), context_requested);
378                control.remove_handler(InputElement::context_canceled_event(), context_canceled);
379                attached.dispose();
380                detached.dispose();
381            }
382            old_menu.remove_attached_control(&control.to_ref());
383            let popup = old_menu.popup.borrow().clone();
384            if let Some(popup) = popup {
385                popup.set_parent(None);
386            }
387        }
388
389        if new_value.is_some() {
390            let context_requested =
391                control.add_handler(InputElement::context_requested_event(), Self::control_context_requested);
392            let context_canceled =
393                control.add_handler(InputElement::context_canceled_event(), Self::control_context_canceled);
394            let weak = control.to_ref().downgrade();
395            let attached = control.attached_to_visual_tree({
396                let weak = weak.clone();
397                move |e| {
398                    if let Some(control) = weak.upgrade() {
399                        Self::control_on_attached_to_visual_tree(&control, e);
400                    }
401                }
402            });
403            let detached = control.detached_from_visual_tree(move |e| {
404                if let Some(control) = weak.upgrade() {
405                    Self::control_detached_from_visual_tree(&control, e);
406                }
407            });
408            control.context_menu_handlers().set(Some((context_requested, context_canceled, attached, detached)));
409        }
410
411        if control.is_attached_to_visual_tree() {
412            Self::attach_control_to_context_menu(control);
413        }
414    }
415
416    /// Opens a context menu on the specified control.
417    ///
418    /// # Panics
419    ///
420    /// Panics when `control` is `None` and the menu is attached to no
421    /// control, and when `control` is not one of the controls the menu is
422    /// attached to.
423    pub fn open_at(&self, control: Option<&Ref<Control>>) {
424        let attached_controls = self.attached_controls();
425
426        if control.is_none() && attached_controls.as_ref().is_none_or(|controls| controls.is_empty()) {
427            panic!("Value cannot be null. (Parameter 'control')");
428        }
429
430        if let (Some(control), Some(attached_controls)) = (control, &attached_controls) {
431            if !attached_controls.contains(control) {
432                panic!(
433                    "Cannot show ContentMenu on a different control to the one it is attached to. (Parameter 'control')"
434                );
435            }
436        }
437
438        let control = match control {
439            Some(control) => control.clone(),
440            None => attached_controls.expect("checked above")[0].clone(),
441        };
442        let placement_target = self.placement_target().unwrap_or_else(|| control.clone());
443        self.open_core(&control, &placement_target, self.placement(), None);
444    }
445
446    /// The host of the popup the menu is shown in, while it is open.
447    pub fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
448        let popup = self.popup.borrow().clone();
449        popup.and_then(|popup| popup.host())
450    }
451
452    /// Raised when the popup host changes (the popup host provider
453    /// contract). Disposing the returned handle unsubscribes.
454    pub fn popup_host_changed(
455        &self,
456        handler: impl Fn(Option<&Rc<dyn IPopupHost>>) + 'static,
457    ) -> Rc<dyn IDisposable> {
458        self.subscribe::<dyn Fn(Option<&Rc<dyn IPopupHost>>)>(|menu| &menu.popup_host_changed_handler, Rc::new(handler))
459    }
460
461    /// The menu as a popup host provider (the diagnostics contract it
462    /// implements in the reference).
463    pub fn to_popup_host_provider(&self) -> Rc<dyn IPopupHostProvider> {
464        Rc::new(ContextMenuHostProvider(self.to_ref()))
465    }
466
467    fn open_core(
468        &self,
469        control: &Ref<Control>,
470        placement_target: &Ref<Control>,
471        placement: PlacementMode,
472        placement_rect: Option<Rect>,
473    ) {
474        if self.is_open() {
475            return;
476        }
477
478        let existing = self.popup.borrow().clone();
479        let popup = match existing {
480            Some(popup) => popup,
481            None => {
482                let popup = Popup::new();
483                popup.set_is_light_dismiss_enabled(true);
484                popup.set_overlay_dismiss_event_pass_through(true);
485                popup.set_takes_focus_from_native_control(Popup::get_takes_focus_from_native_control(self));
486
487                // The popup belongs to the menu and lives as long as it:
488                // the handlers are never removed.
489                let weak = self.to_ref().downgrade();
490                let _ = popup.opened({
491                    let weak = weak.clone();
492                    move || {
493                        if let Some(this) = weak.upgrade() {
494                            this.popup_opened();
495                        }
496                    }
497                });
498                let _ = popup.closed({
499                    let weak = weak.clone();
500                    move || {
501                        if let Some(this) = weak.upgrade() {
502                            this.popup_closed();
503                        }
504                    }
505                });
506                let _ = popup.closing({
507                    let weak = weak.clone();
508                    move |e| {
509                        if let Some(this) = weak.upgrade() {
510                            this.popup_closing(e);
511                        }
512                    }
513                });
514                popup.add_handler(InputElement::key_up_event(), move |_, e: &KeyEventArgs| {
515                    if let Some(this) = weak.upgrade() {
516                        this.popup_key_up(e);
517                    }
518                });
519
520                *self.popup.borrow_mut() = Some(popup.clone());
521                popup
522            }
523        };
524
525        popup.set_popup_parent(Some(control));
526
527        popup.set_placement(placement);
528
529        // Position of the line below is really important.
530        // All styles are being applied only when control has logical parent.
531        // Line below will add ContextMenu as child to the Popup and this will trigger styles and they would be applied.
532        // If you will move line below somewhere else it may cause that ContextMenu will behave differently from what you are expecting.
533        popup.set_child(self.to_ref());
534        popup.set_placement_target(placement_target);
535        popup.set_horizontal_offset(self.horizontal_offset());
536        popup.set_vertical_offset(self.vertical_offset());
537        popup.set_placement_anchor(self.placement_anchor());
538        popup.set_placement_constraint_adjustment(self.placement_constraint_adjustment());
539        popup.set_placement_gravity(self.placement_gravity());
540        popup.set_placement_rect(placement_rect.or_else(|| self.placement_rect()));
541        popup.set_custom_popup_placement_callback(self.custom_popup_placement_callback());
542        popup.set_window_manager_add_shadow_hint(self.window_manager_add_shadow_hint());
543        self.set_is_open(true);
544        popup.set_is_open(true);
545
546        self.raise_event(&RoutedEventArgs::with_event_and_source(MenuBase::opened_event(), self.to_ref()));
547    }
548
549    fn popup_opened(&self) {
550        let previous_focus = FocusManager::get_focus_manager(self).and_then(|manager| manager.get_focused_element());
551        *self.previous_focus.borrow_mut() = previous_focus.map(|element| element.downgrade());
552        self.focus();
553
554        let host = self.popup_host();
555        for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
556            handler(host.as_ref());
557        }
558    }
559
560    fn popup_closing(&self, e: &CancelEventArgs) {
561        e.set_cancel(self.cancel_closing());
562    }
563
564    fn popup_closed(&self) {
565        let children = StyledElement::logical_children(self).to_vec();
566        for i in children {
567            if let Some(menu_item) = i.cast::<MenuItem>() {
568                menu_item.set_is_sub_menu_open(false);
569            }
570        }
571
572        self.set_selected_index(-1);
573        self.set_is_open(false);
574
575        let popup = self.popup.borrow().clone();
576
577        if self.attached_controls().is_none_or(|controls| controls.is_empty()) {
578            if let Some(popup) = &popup {
579                popup.set_popup_parent(None);
580            }
581        }
582
583        self.raise_event(&RoutedEventArgs::with_event_and_source(MenuBase::closed_event(), self.to_ref()));
584
585        for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
586            handler(None);
587        }
588
589        // Not in the reference, where the popup keeps the menu as its
590        // child: the menu owns the popup, so the popup holds the menu only
591        // while it is open (a menu and its popup must not keep each other
592        // alive). A handler above may have opened the menu again.
593        if let Some(popup) = popup {
594            if !popup.is_open() && !self.is_open() {
595                popup.set_child(None::<Ref<Control>>);
596            }
597        }
598    }
599
600    fn popup_key_up(&self, e: &KeyEventArgs) {
601        if self.is_open() {
602            let keymap = Application::current()
603                .and_then(|application| application.platform_settings())
604                .map(|settings| settings.hotkey_configuration());
605
606            if keymap.is_some_and(|keymap| keymap.open_context_menu.iter().any(|k| k.matches(Some(e))))
607                && !self.cancel_closing()
608            {
609                self.close();
610                e.set_handled(true);
611            }
612        }
613    }
614
615    fn control_context_canceled(sender: &Interactive, e: &RoutedEventArgs) {
616        if e.handled() {
617            return;
618        }
619        let Some(control) = sender.downcast_ref::<Control>() else { return };
620        let Some(context_menu) = control.context_menu() else { return };
621
622        if context_menu.is_open() {
623            context_menu.close();
624        }
625    }
626
627    pub(crate) fn control_context_requested(sender: &Interactive, e: &ContextRequestedEventArgs) {
628        if e.handled() {
629            return;
630        }
631        let Some(control) = sender.downcast_ref::<Control>() else { return };
632        let Some(context_menu) = control.context_menu() else { return };
633        let root = control.visual_root();
634        let request_generation = control.context_request_generation();
635        let is_current_request = || {
636            root == control.visual_root()
637                && control.context_request_generation() == request_generation
638                && control.context_menu() == Some(context_menu.clone())
639        };
640
641        if !context_menu.cancel_opening() {
642            if e.handled() || !is_current_request() {
643                e.set_handled(true);
644                return;
645            }
646            let requested_by_pointer = e.try_get_position(None).is_some();
647            let control = control.to_ref();
648            let placement_target =
649                e.source().and_then(|source| source.cast::<Control>()).unwrap_or_else(|| control.clone());
650            let placement_root = placement_target.visual_root();
651            let placement_rect = e.try_get_placement_rect(&placement_target);
652            // Anchor conversion reads application-supplied ITransform values.
653            // Keep a newer rich request or detached target from being replaced
654            // by this older invocation after its typed opening event returned.
655            if e.handled()
656                || !is_current_request()
657                || placement_root != placement_target.visual_root()
658            {
659                e.set_handled(true);
660                return;
661            }
662            context_menu.open_core(
663                &control,
664                &placement_target,
665                if placement_rect.is_some() {
666                    // The popup positioner mirrors this alignment for RTL.
667                    PlacementMode::BottomEdgeAlignedLeft
668                } else if requested_by_pointer {
669                    context_menu.placement()
670                } else {
671                    PlacementMode::Bottom
672                },
673                placement_rect,
674            );
675            e.set_handled(true);
676
677            if e.is_holding() {
678                control.perform_feedback(FeedbackAction::hold());
679            }
680        }
681    }
682
683    fn control_on_attached_to_visual_tree(sender: &Control, _e: &VisualTreeAttachmentEventArgs) {
684        Self::attach_control_to_context_menu(sender);
685    }
686
687    fn attach_control_to_context_menu(control: &Control) {
688        if let Some(context_menu) = control.context_menu() {
689            context_menu
690                .attached_controls
691                .borrow_mut()
692                .get_or_insert_with(Vec::new)
693                .push(control.to_ref().downgrade());
694        }
695    }
696
697    fn control_detached_from_visual_tree(control: &Control, _e: &VisualTreeAttachmentEventArgs) {
698        if let Some(context_menu) = control.context_menu() {
699            let control = control.to_ref();
700            let popup = context_menu.popup.borrow().clone();
701            if let Some(popup) = popup {
702                if popup.parent() == Some(control.clone().upcast::<StyledElement>()) {
703                    popup.set_parent(None);
704                }
705            }
706
707            context_menu.close();
708            context_menu.remove_attached_control(&control);
709        }
710    }
711
712    fn cancel_closing(&self) -> bool {
713        let event_args = CancelEventArgs::new();
714        for (_, handler) in self.closing.snapshot().iter() {
715            handler(&event_args);
716        }
717        event_args.cancel()
718    }
719
720    fn cancel_opening(&self) -> bool {
721        let event_args = CancelEventArgs::new();
722        for (_, handler) in self.opening.snapshot().iter() {
723            handler(&event_args);
724        }
725        event_args.cancel()
726    }
727}
728
729/// The popup host provider contract of a context menu.
730struct ContextMenuHostProvider(Ref<ContextMenu>);
731
732impl IPopupHostProvider for ContextMenuHostProvider {
733    fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
734        self.0.popup_host()
735    }
736
737    fn popup_host_changed(&self, handler: Rc<dyn Fn(Option<&Rc<dyn IPopupHost>>)>) -> Rc<dyn IDisposable> {
738        self.0.subscribe::<dyn Fn(Option<&Rc<dyn IPopupHost>>)>(|menu| &menu.popup_host_changed_handler, handler)
739    }
740}