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, VisualTreeAttachmentEventArgs, WeakRef,
18};
19use std::cell::RefCell;
20use std::rc::Rc;
21
22#[repr(C)]
24pub struct ContextMenu {
25 base: MenuBase,
26 popup: RefCell<Option<Ref<Popup>>>,
27 attached_controls: RefCell<Option<Vec<WeakRef<Control>>>>,
31 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}
38
39ferro_class!(ContextMenu: MenuBase);
40ferro_class_info!(ContextMenu { new: ContextMenu::new });
41
42ferro_impl_classes!(
43 ContextMenu: StyledElementImpl,
44 VisualImpl,
45 LayoutableImpl,
46 InteractiveImpl,
47 InputElementImpl,
48 ControlImpl,
49 TemplatedControlImpl,
50 ItemsControlImpl,
51 SelectingItemsControlImpl
52);
53
54impl FerroObjectImpl for ContextMenu {
55 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
56 Self::parent_on_property_changed(this, change);
57
58 if change.property() == Self::window_manager_add_shadow_hint_property().as_property() {
59 let popup = this.popup.borrow().clone();
60 if let Some(popup) = popup {
61 popup.set_window_manager_add_shadow_hint(change.get_new_value::<bool>());
62 }
63 }
64 }
65}
66
67impl MenuBaseImpl for ContextMenu {
68 fn open(this: &Self) {
70 this.open_at(None);
71 }
72
73 fn close(this: &Self) {
75 if !this.is_open() {
76 return;
77 }
78
79 let popup = this.popup.borrow().clone();
80 if let Some(popup) = popup {
81 if popup.is_visible() {
82 popup.set_is_open(false);
83 }
84 }
85 }
86}
87
88impl ISetterValue for ContextMenu {
89 fn initialize(&self, setter: &Setter) {
90 if setter.property() != Some(Control::context_menu_property().as_property()) {
93 panic!("Cannot use a control as a Setter value. Wrap the control in a <Template>.");
94 }
95 }
96}
97
98ferro_properties! {
99 impl ContextMenu {
100 pub fn horizontal_offset_property() -> StyledProperty<f64> {
102 Popup::horizontal_offset_property().add_owner::<ContextMenu>()
103 }
104
105 pub fn vertical_offset_property() -> StyledProperty<f64> {
107 Popup::vertical_offset_property().add_owner::<ContextMenu>()
108 }
109
110 pub fn placement_anchor_property() -> StyledProperty<PopupAnchor> {
112 Popup::placement_anchor_property().add_owner::<ContextMenu>()
113 }
114
115 pub fn placement_constraint_adjustment_property() -> StyledProperty<PopupPositionerConstraintAdjustment> {
117 Popup::placement_constraint_adjustment_property().add_owner::<ContextMenu>()
118 }
119
120 pub fn placement_gravity_property() -> StyledProperty<PopupGravity> {
122 Popup::placement_gravity_property().add_owner::<ContextMenu>()
123 }
124
125 pub fn placement_property() -> StyledProperty<PlacementMode> {
127 Popup::placement_property().add_owner::<ContextMenu>()
128 }
129
130 pub fn placement_rect_property() -> StyledProperty<Option<Rect>> {
132 Popup::placement_rect_property().add_owner::<ContextMenu>()
133 }
134
135 pub fn window_manager_add_shadow_hint_property() -> StyledProperty<bool> {
137 Popup::window_manager_add_shadow_hint_property().add_owner::<ContextMenu>()
138 }
139
140 pub fn placement_target_property() -> StyledProperty<Option<ElementRef<Control>>> {
145 Popup::placement_target_property().add_owner::<ContextMenu>()
146 }
147
148 pub fn custom_popup_placement_callback_property() -> StyledProperty<Option<CustomPopupPlacementCallbackValue>> {
150 Popup::custom_popup_placement_callback_property().add_owner::<ContextMenu>()
151 }
152 }
153}
154
155impl ContextMenu {
156 fn static_constructor() {
158 Self::placement_property().override_default_value::<ContextMenu>(PlacementMode::Pointer);
159 let _ = Control::context_menu_property().changed().subscribe(Self::context_menu_changed);
160 crate::automation::AutomationProperties::accessibility_view_property()
161 .override_default_value::<ContextMenu>(crate::automation::AccessibilityView::Control);
162 crate::automation::AutomationProperties::control_type_override_property()
163 .override_default_value::<ContextMenu>(Some(crate::automation::peers::AutomationControlType::Menu));
164 }
165
166 pub fn construct() -> Self {
168 Self::construct_with(DefaultMenuInteractionHandler::new(true))
169 }
170
171 pub fn construct_with(interaction_handler: Rc<dyn IMenuInteractionHandler>) -> Self {
174 Self {
175 base: MenuBase::construct_with(interaction_handler),
176 popup: RefCell::new(None),
177 attached_controls: RefCell::new(None),
178 previous_focus: RefCell::new(None),
179 popup_host_changed_handler: HandlerList::new(),
180 opening: HandlerList::new(),
181 closing: HandlerList::new(),
182 }
183 }
184
185 pub fn new() -> Ref<Self> {
187 instantiate(Self::construct())
188 }
189
190 pub fn with_interaction_handler(interaction_handler: Rc<dyn IMenuInteractionHandler>) -> Ref<Self> {
193 instantiate(Self::construct_with(interaction_handler))
194 }
195
196 pub fn horizontal_offset(&self) -> f64 {
199 self.get_value(Self::horizontal_offset_property())
200 }
201
202 pub fn set_horizontal_offset(&self, value: f64) {
203 self.set_value(Self::horizontal_offset_property(), value)
204 }
205
206 pub fn vertical_offset(&self) -> f64 {
209 self.get_value(Self::vertical_offset_property())
210 }
211
212 pub fn set_vertical_offset(&self, value: f64) {
213 self.set_value(Self::vertical_offset_property(), value)
214 }
215
216 pub fn placement_anchor(&self) -> PopupAnchor {
218 self.get_value(Self::placement_anchor_property())
219 }
220
221 pub fn set_placement_anchor(&self, value: PopupAnchor) {
222 self.set_value(Self::placement_anchor_property(), value)
223 }
224
225 pub fn placement_constraint_adjustment(&self) -> PopupPositionerConstraintAdjustment {
227 self.get_value(Self::placement_constraint_adjustment_property())
228 }
229
230 pub fn set_placement_constraint_adjustment(&self, value: PopupPositionerConstraintAdjustment) {
231 self.set_value(Self::placement_constraint_adjustment_property(), value)
232 }
233
234 pub fn placement_gravity(&self) -> PopupGravity {
236 self.get_value(Self::placement_gravity_property())
237 }
238
239 pub fn set_placement_gravity(&self, value: PopupGravity) {
240 self.set_value(Self::placement_gravity_property(), value)
241 }
242
243 pub fn placement(&self) -> PlacementMode {
245 self.get_value(Self::placement_property())
246 }
247
248 pub fn set_placement(&self, value: PlacementMode) {
249 self.set_value(Self::placement_property(), value)
250 }
251
252 pub fn window_manager_add_shadow_hint(&self) -> bool {
254 self.get_value(Self::window_manager_add_shadow_hint_property())
255 }
256
257 pub fn set_window_manager_add_shadow_hint(&self, value: bool) {
258 self.set_value(Self::window_manager_add_shadow_hint_property(), value)
259 }
260
261 pub fn placement_rect(&self) -> Option<Rect> {
263 self.get_value(Self::placement_rect_property())
264 }
265
266 pub fn set_placement_rect(&self, value: Option<Rect>) {
267 self.set_value(Self::placement_rect_property(), value)
268 }
269
270 pub fn placement_target(&self) -> Option<Ref<Control>> {
272 ElementRef::resolve(&self.get_value(Self::placement_target_property()))
273 }
274
275 pub fn set_placement_target(&self, value: impl Into<Nullable<Control>>) {
276 self.set_value(Self::placement_target_property(), ElementRef::from_nullable(value.into().0))
277 }
278
279 pub fn custom_popup_placement_callback(&self) -> Option<CustomPopupPlacementCallback> {
281 self.get_value(Self::custom_popup_placement_callback_property()).map(|value| value.0)
282 }
283
284 pub fn set_custom_popup_placement_callback(&self, value: Option<CustomPopupPlacementCallback>) {
285 self.set_value(
286 Self::custom_popup_placement_callback_property(),
287 value.map(CustomPopupPlacementCallbackValue),
288 )
289 }
290
291 fn subscribe<F: ?Sized + 'static>(
292 &self,
293 select: fn(&ContextMenu) -> &HandlerList<F>,
294 handler: Rc<F>,
295 ) -> Rc<dyn IDisposable> {
296 let token = select(self).add(handler);
297 let weak = self.to_ref().downgrade();
298 Disposable::create(move || {
299 if let Some(this) = weak.upgrade() {
300 select(&this).remove(token);
301 }
302 })
303 }
304
305 pub fn opening(&self, handler: impl Fn(&CancelEventArgs) + 'static) -> Rc<dyn IDisposable> {
308 self.subscribe::<dyn Fn(&CancelEventArgs)>(|menu| &menu.opening, Rc::new(handler))
309 }
310
311 pub fn closing(&self, handler: impl Fn(&CancelEventArgs) + 'static) -> Rc<dyn IDisposable> {
314 self.subscribe::<dyn Fn(&CancelEventArgs)>(|menu| &menu.closing, Rc::new(handler))
315 }
316
317 fn attached_controls(&self) -> Option<Vec<Ref<Control>>> {
320 self.attached_controls.borrow().as_ref().map(|controls| controls.iter().filter_map(WeakRef::upgrade).collect())
321 }
322
323 fn remove_attached_control(&self, control: &Ref<Control>) {
326 if let Some(controls) = self.attached_controls.borrow_mut().as_mut() {
327 controls.retain(|c| c.upgrade().is_some());
329 if let Some(index) = controls.iter().position(|c| c.upgrade().as_ref() == Some(control)) {
330 controls.remove(index);
331 }
332 }
333 }
334
335 fn context_menu_changed(e: &FerroPropertyChangedEventArgs<'_>) {
337 let control = e.sender().downcast_ref::<Control>().expect("the sender is a control");
338 let (old_value, new_value) = e.get_old_and_new_value::<Option<Ref<ContextMenu>>>();
339
340 if let Some(old_menu) = old_value {
341 if let Some((context_requested, context_canceled, attached, detached)) =
342 control.context_menu_handlers().take()
343 {
344 control.remove_handler(InputElement::context_requested_event(), context_requested);
345 control.remove_handler(InputElement::context_canceled_event(), context_canceled);
346 attached.dispose();
347 detached.dispose();
348 }
349 old_menu.remove_attached_control(&control.to_ref());
350 let popup = old_menu.popup.borrow().clone();
351 if let Some(popup) = popup {
352 popup.set_parent(None);
353 }
354 }
355
356 if new_value.is_some() {
357 let context_requested =
358 control.add_handler(InputElement::context_requested_event(), Self::control_context_requested);
359 let context_canceled =
360 control.add_handler(InputElement::context_canceled_event(), Self::control_context_canceled);
361 let weak = control.to_ref().downgrade();
362 let attached = control.attached_to_visual_tree({
363 let weak = weak.clone();
364 move |e| {
365 if let Some(control) = weak.upgrade() {
366 Self::control_on_attached_to_visual_tree(&control, e);
367 }
368 }
369 });
370 let detached = control.detached_from_visual_tree(move |e| {
371 if let Some(control) = weak.upgrade() {
372 Self::control_detached_from_visual_tree(&control, e);
373 }
374 });
375 control.context_menu_handlers().set(Some((context_requested, context_canceled, attached, detached)));
376 }
377
378 if control.is_attached_to_visual_tree() {
379 Self::attach_control_to_context_menu(control);
380 }
381 }
382
383 pub fn open_at(&self, control: Option<&Ref<Control>>) {
391 let attached_controls = self.attached_controls();
392
393 if control.is_none() && attached_controls.as_ref().is_none_or(|controls| controls.is_empty()) {
394 panic!("Value cannot be null. (Parameter 'control')");
395 }
396
397 if let (Some(control), Some(attached_controls)) = (control, &attached_controls) {
398 if !attached_controls.contains(control) {
399 panic!(
400 "Cannot show ContentMenu on a different control to the one it is attached to. (Parameter 'control')"
401 );
402 }
403 }
404
405 let control = match control {
406 Some(control) => control.clone(),
407 None => attached_controls.expect("checked above")[0].clone(),
408 };
409 let placement_target = self.placement_target().unwrap_or_else(|| control.clone());
410 self.open_core(&control, &placement_target, self.placement());
411 }
412
413 pub fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
415 let popup = self.popup.borrow().clone();
416 popup.and_then(|popup| popup.host())
417 }
418
419 pub fn popup_host_changed(
422 &self,
423 handler: impl Fn(Option<&Rc<dyn IPopupHost>>) + 'static,
424 ) -> Rc<dyn IDisposable> {
425 self.subscribe::<dyn Fn(Option<&Rc<dyn IPopupHost>>)>(|menu| &menu.popup_host_changed_handler, Rc::new(handler))
426 }
427
428 pub fn to_popup_host_provider(&self) -> Rc<dyn IPopupHostProvider> {
431 Rc::new(ContextMenuHostProvider(self.to_ref()))
432 }
433
434 fn open_core(&self, control: &Ref<Control>, placement_target: &Ref<Control>, placement: PlacementMode) {
435 if self.is_open() {
436 return;
437 }
438
439 let existing = self.popup.borrow().clone();
440 let popup = match existing {
441 Some(popup) => popup,
442 None => {
443 let popup = Popup::new();
444 popup.set_is_light_dismiss_enabled(true);
445 popup.set_overlay_dismiss_event_pass_through(true);
446 popup.set_takes_focus_from_native_control(Popup::get_takes_focus_from_native_control(self));
447
448 let weak = self.to_ref().downgrade();
451 let _ = popup.opened({
452 let weak = weak.clone();
453 move || {
454 if let Some(this) = weak.upgrade() {
455 this.popup_opened();
456 }
457 }
458 });
459 let _ = popup.closed({
460 let weak = weak.clone();
461 move || {
462 if let Some(this) = weak.upgrade() {
463 this.popup_closed();
464 }
465 }
466 });
467 let _ = popup.closing({
468 let weak = weak.clone();
469 move |e| {
470 if let Some(this) = weak.upgrade() {
471 this.popup_closing(e);
472 }
473 }
474 });
475 popup.add_handler(InputElement::key_up_event(), move |_, e: &KeyEventArgs| {
476 if let Some(this) = weak.upgrade() {
477 this.popup_key_up(e);
478 }
479 });
480
481 *self.popup.borrow_mut() = Some(popup.clone());
482 popup
483 }
484 };
485
486 popup.set_popup_parent(Some(control));
487
488 popup.set_placement(placement);
489
490 popup.set_child(self.to_ref());
495 popup.set_placement_target(placement_target);
496 popup.set_horizontal_offset(self.horizontal_offset());
497 popup.set_vertical_offset(self.vertical_offset());
498 popup.set_placement_anchor(self.placement_anchor());
499 popup.set_placement_constraint_adjustment(self.placement_constraint_adjustment());
500 popup.set_placement_gravity(self.placement_gravity());
501 popup.set_placement_rect(self.placement_rect());
502 popup.set_custom_popup_placement_callback(self.custom_popup_placement_callback());
503 popup.set_window_manager_add_shadow_hint(self.window_manager_add_shadow_hint());
504 self.set_is_open(true);
505 popup.set_is_open(true);
506
507 self.raise_event(&RoutedEventArgs::with_event_and_source(MenuBase::opened_event(), self.to_ref()));
508 }
509
510 fn popup_opened(&self) {
511 let previous_focus = FocusManager::get_focus_manager(self).and_then(|manager| manager.get_focused_element());
512 *self.previous_focus.borrow_mut() = previous_focus.map(|element| element.downgrade());
513 self.focus();
514
515 let host = self.popup_host();
516 for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
517 handler(host.as_ref());
518 }
519 }
520
521 fn popup_closing(&self, e: &CancelEventArgs) {
522 e.set_cancel(self.cancel_closing());
523 }
524
525 fn popup_closed(&self) {
526 let children = StyledElement::logical_children(self).to_vec();
527 for i in children {
528 if let Some(menu_item) = i.cast::<MenuItem>() {
529 menu_item.set_is_sub_menu_open(false);
530 }
531 }
532
533 self.set_selected_index(-1);
534 self.set_is_open(false);
535
536 let popup = self.popup.borrow().clone();
537
538 if self.attached_controls().is_none_or(|controls| controls.is_empty()) {
539 if let Some(popup) = &popup {
540 popup.set_popup_parent(None);
541 }
542 }
543
544 self.raise_event(&RoutedEventArgs::with_event_and_source(MenuBase::closed_event(), self.to_ref()));
545
546 for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
547 handler(None);
548 }
549
550 if let Some(popup) = popup {
555 if !popup.is_open() && !self.is_open() {
556 popup.set_child(None::<Ref<Control>>);
557 }
558 }
559 }
560
561 fn popup_key_up(&self, e: &KeyEventArgs) {
562 if self.is_open() {
563 let keymap = Application::current()
564 .and_then(|application| application.platform_settings())
565 .map(|settings| settings.hotkey_configuration());
566
567 if keymap.is_some_and(|keymap| keymap.open_context_menu.iter().any(|k| k.matches(Some(e))))
568 && !self.cancel_closing()
569 {
570 self.close();
571 e.set_handled(true);
572 }
573 }
574 }
575
576 fn control_context_canceled(sender: &Interactive, e: &RoutedEventArgs) {
577 if e.handled() {
578 return;
579 }
580 let Some(control) = sender.downcast_ref::<Control>() else { return };
581 let Some(context_menu) = control.context_menu() else { return };
582
583 if context_menu.is_open() {
584 context_menu.close();
585 }
586 }
587
588 fn control_context_requested(sender: &Interactive, e: &ContextRequestedEventArgs) {
589 let Some(control) = sender.downcast_ref::<Control>() else { return };
590 let Some(context_menu) = control.context_menu() else { return };
591
592 if !e.handled() && !context_menu.cancel_opening() {
593 let requested_by_pointer = e.try_get_position(None).is_some();
594 let control = control.to_ref();
595 let placement_target =
596 e.source().and_then(|source| source.cast::<Control>()).unwrap_or_else(|| control.clone());
597 context_menu.open_core(
598 &control,
599 &placement_target,
600 if requested_by_pointer { context_menu.placement() } else { PlacementMode::Bottom },
601 );
602 e.set_handled(true);
603
604 if e.is_holding() {
605 control.perform_feedback(FeedbackAction::hold());
606 }
607 }
608 }
609
610 fn control_on_attached_to_visual_tree(sender: &Control, _e: &VisualTreeAttachmentEventArgs) {
611 Self::attach_control_to_context_menu(sender);
612 }
613
614 fn attach_control_to_context_menu(control: &Control) {
615 if let Some(context_menu) = control.context_menu() {
616 context_menu
617 .attached_controls
618 .borrow_mut()
619 .get_or_insert_with(Vec::new)
620 .push(control.to_ref().downgrade());
621 }
622 }
623
624 fn control_detached_from_visual_tree(control: &Control, _e: &VisualTreeAttachmentEventArgs) {
625 if let Some(context_menu) = control.context_menu() {
626 let control = control.to_ref();
627 let popup = context_menu.popup.borrow().clone();
628 if let Some(popup) = popup {
629 if popup.parent() == Some(control.clone().upcast::<StyledElement>()) {
630 popup.set_parent(None);
631 }
632 }
633
634 context_menu.close();
635 context_menu.remove_attached_control(&control);
636 }
637 }
638
639 fn cancel_closing(&self) -> bool {
640 let event_args = CancelEventArgs::new();
641 for (_, handler) in self.closing.snapshot().iter() {
642 handler(&event_args);
643 }
644 event_args.cancel()
645 }
646
647 fn cancel_opening(&self) -> bool {
648 let event_args = CancelEventArgs::new();
649 for (_, handler) in self.opening.snapshot().iter() {
650 handler(&event_args);
651 }
652 event_args.cancel()
653 }
654}
655
656struct ContextMenuHostProvider(Ref<ContextMenu>);
658
659impl IPopupHostProvider for ContextMenuHostProvider {
660 fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
661 self.0.popup_host()
662 }
663
664 fn popup_host_changed(&self, handler: Rc<dyn Fn(Option<&Rc<dyn IPopupHost>>)>) -> Rc<dyn IDisposable> {
665 self.0.subscribe::<dyn Fn(Option<&Rc<dyn IPopupHost>>)>(|menu| &menu.popup_host_changed_handler, handler)
666 }
667}