1#[cfg(wayland_platform)]
7use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem};
8use crate::widget::menu::{
9 self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight,
10 init_root_menu, menu_roots_diff,
11};
12use derive_setters::Setters;
13use iced::touch::Finger;
14use iced::{Event, Vector, keyboard, window};
15use iced_core::widget::{Tree, Widget, tree};
16use iced_core::{Length, Point, Size, mouse, touch};
17use std::collections::HashSet;
18use std::sync::Arc;
19
20use crate::widget::RcWrapper;
21
22pub fn context_menu<'a, Message: 'static + Clone>(
24 content: impl Into<crate::Element<'a, Message>>,
25 context_menu: Option<Vec<menu::Tree<Message>>>,
27) -> ContextMenu<'a, Message> {
28 let mut this = ContextMenu {
29 content: content.into(),
30 context_menu: context_menu.map(|menus| {
31 vec![menu::Tree::with_children(
32 crate::Element::from(crate::widget::Row::new()),
33 menus,
34 )]
35 }),
36 close_on_escape: true,
37 window_id: window::Id::RESERVED,
38 item_width: ItemWidth::Uniform(240),
39 on_open: None,
40 on_close: None,
41 on_surface_action: None,
42 };
43
44 if let Some(ref mut context_menu) = this.context_menu {
45 context_menu.iter_mut().for_each(menu::Tree::set_index);
46 }
47
48 this
49}
50
51#[derive(Setters)]
53#[must_use]
54pub struct ContextMenu<'a, Message> {
55 #[setters(skip)]
56 content: crate::Element<'a, Message>,
57 #[setters(skip)]
58 context_menu: Option<Vec<menu::Tree<Message>>>,
59 pub window_id: window::Id,
60 pub close_on_escape: bool,
61 pub item_width: ItemWidth,
63 #[setters(strip_option)]
65 pub on_open: Option<Message>,
66 #[setters(strip_option)]
68 pub on_close: Option<Message>,
69 #[setters(skip)]
70 pub(crate) on_surface_action:
71 Option<Arc<dyn Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static>>,
72}
73
74impl<Message: Clone + 'static> ContextMenu<'_, Message> {
75 fn report_open_state(
77 &self,
78 state: &mut LocalState<Message>,
79 shell: &mut iced_core::Shell<'_, Message>,
80 ) {
81 let open = state.menu_bar_state.inner.with_data(|d| d.open);
82 if open == state.reported_open {
83 return;
84 }
85 state.reported_open = open;
86 let message = if open { &self.on_open } else { &self.on_close };
87 if let Some(message) = message.clone() {
88 shell.publish(message);
89 }
90 }
91
92 #[cfg(wayland_platform)]
93 #[allow(clippy::too_many_lines)]
94 fn create_popup(
95 &mut self,
96 layout: iced_core::Layout<'_>,
97 view_cursor: iced_core::mouse::Cursor,
98 renderer: &crate::Renderer,
99 shell: &mut iced_core::Shell<'_, Message>,
100 viewport: &iced::Rectangle,
101 my_state: &mut LocalState<Message>,
102 ) {
103 if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
104 use crate::surface::action::{LiveSettings, destroy_popup};
105 use crate::theme::THEME;
106 use crate::widget::menu::{Menu, StyleSheet as _};
107 use iced_runtime::platform_specific::wayland::CornerRadius;
108 use iced_runtime::platform_specific::wayland::popup::{
109 SctkPopupSettings, SctkPositioner,
110 };
111
112 let mut bounds = layout.bounds();
113 bounds.x = my_state.context_cursor.x;
114 bounds.y = my_state.context_cursor.y;
115
116 let (id, root_list) = my_state.menu_bar_state.inner.with_data_mut(|state| {
117 if let Some(id) = state.popup_id.get(&self.window_id).copied() {
118 state.menu_states.clear();
120 state.active_root.clear();
121
122 shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id)));
123 state.view_cursor = view_cursor;
124 }
125 (
127 window::Id::unique(),
128 layout.children().map(|lo| lo.bounds()).collect::<Vec<_>>(),
129 )
130 });
131 let Some(context_menu) = self.context_menu.as_mut() else {
132 return;
133 };
134
135 let mut popup_menu: Menu<'static, _> = Menu {
136 tree: my_state.menu_bar_state.clone(),
137 menu_roots: std::borrow::Cow::Owned(context_menu.clone()),
138 bounds_expand: 16,
139 menu_overlays_parent: true,
140 close_condition: CloseCondition {
141 leave: false,
142 click_outside: true,
143 click_inside: true,
144 },
145 item_width: self.item_width,
146 item_height: ItemHeight::Dynamic(40),
147 bar_bounds: bounds,
148 main_offset: -(bounds.height as i32),
149 cross_offset: 0,
150 root_bounds_list: vec![bounds],
151 path_highlight: Some(PathHighlight::MenuActive),
152 style: std::borrow::Cow::Owned(crate::theme::menu_bar::MenuBarStyle::Default),
153 position: Point::new(0., 0.),
154 is_overlay: false,
155 window_id: id,
156 depth: 0,
157 on_surface_action: self.on_surface_action.clone(),
158 };
159
160 init_root_menu(
161 &mut popup_menu,
162 renderer,
163 shell,
164 view_cursor.position().unwrap(),
165 viewport.size(),
166 Vector::new(0., 0.),
167 layout.bounds(),
168 -bounds.height,
169 );
170 let (anchor_rect, gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| {
171 use iced::Rectangle;
172
173 state.popup_id.insert(self.window_id, id);
174 ({
175 let pos = view_cursor.position().unwrap_or_default();
176 Rectangle {
177 x: pos.x as i32,
178 y: pos.y as i32,
179 width: 1,
180 height: 1,
181 }
182 },
183 match (state.horizontal_direction, state.vertical_direction) {
184 (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
185 (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
186 (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
187 (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
188 })
189 });
190
191 let menu_node =
192 popup_menu.layout(renderer, iced::Limits::NONE.min_width(1.).min_height(1.));
193 let popup_size = menu_node.size();
194 let positioner = SctkPositioner {
195 size: Some((
196 popup_size.width.ceil() as u32 + 2,
197 popup_size.height.ceil() as u32 + 2,
198 )),
199 anchor_rect,
200 anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::None,
201 gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
202 reactive: true,
203 ..Default::default()
204 };
205 let parent = self.window_id;
206 let roots = my_state.roots.clone();
207 let t = THEME.lock().unwrap();
208 let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
209 drop(t);
210 let rad = styling.menu_border_radius;
211
212 shell.publish((self.on_surface_action.as_ref().unwrap())(
213 crate::surface::action::simple_popup(
214 move || LiveSettings {
215 corners: Some(CornerRadius {
216 top_left: rad[0] as u32,
217 top_right: rad[1] as u32,
218 bottom_left: rad[2] as u32,
219 bottom_right: rad[3] as u32,
220 }),
221 ..Default::default()
222 },
223 move || SctkPopupSettings {
224 parent,
225 id,
226 positioner: positioner.clone(),
227 parent_size: None,
228 grab: true,
229 close_with_children: false,
230 input_zone: None,
231 },
232 Some(move || {
233 let mut popup_menu = popup_menu.clone();
235 popup_menu.menu_roots =
236 std::borrow::Cow::Owned(roots.with_data(Clone::clone));
237 crate::Element::from(
238 crate::widget::container(popup_menu).center(Length::Fill),
239 )
240 .map(crate::action::app)
241 }),
242 ),
243 ));
244 }
245 }
246
247 pub fn on_surface_action(
248 mut self,
249 handler: impl Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static,
250 ) -> Self {
251 self.on_surface_action = Some(Arc::new(handler));
252 self
253 }
254}
255
256impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
257 for ContextMenu<'_, Message>
258{
259 fn tag(&self) -> tree::Tag {
260 tree::Tag::of::<LocalState<Message>>()
261 }
262
263 fn state(&self) -> tree::State {
264 #[allow(clippy::default_trait_access)]
265 tree::State::new(LocalState {
266 context_cursor: Point::default(),
267 fingers_pressed: Default::default(),
268 menu_bar_state: Default::default(),
269 reported_open: false,
270 roots: RcWrapper::new(self.context_menu.clone().unwrap_or_default()),
271 reshape: false,
272 })
273 }
274
275 fn children(&self) -> Vec<Tree> {
276 let mut children = Vec::with_capacity(if self.context_menu.is_some() { 2 } else { 1 });
277
278 children.push(Tree::new(self.content.as_widget()));
279
280 if let Some(ref context_menu) = self.context_menu {
282 let mut tree = Tree::empty();
283 tree.children = context_menu
284 .iter()
285 .map(|root| {
286 let mut tree = Tree::empty();
287 let flat = root
288 .flattern()
289 .iter()
290 .map(|mt| Tree::new(mt.item.clone()))
291 .collect();
292 tree.children = flat;
293 tree
294 })
295 .collect();
296
297 children.push(tree);
298 }
299
300 children
301 }
302
303 fn diff(&mut self, tree: &mut Tree) {
304 tree.diff_children(std::slice::from_mut(&mut self.content));
305 let state = tree.state.downcast_mut::<LocalState<Message>>();
306 if let Some(context_menu) = self.context_menu.as_mut() {
307 let popup_open = state
312 .menu_bar_state
313 .inner
314 .with_data(|d| !d.popup_id.is_empty());
315 let shape = |roots: &Vec<menu::Tree<Message>>| -> Vec<tree::Tag> {
316 roots.first().map_or_else(Vec::new, |root| {
317 root.flattern().iter().map(|mt| mt.item.tag()).collect()
318 })
319 };
320 if popup_open && state.roots.with_data(shape) != shape(context_menu) {
321 state.reshape = true;
322 return;
323 }
324 state.menu_bar_state.inner.with_data_mut(|inner| {
325 menu_roots_diff(context_menu, &mut inner.tree);
326 });
327 state
328 .roots
329 .with_data_mut(|roots| roots.clone_from(context_menu));
330 }
331
332 }
341
342 fn size(&self) -> Size<Length> {
343 self.content.as_widget().size()
344 }
345
346 fn layout(
347 &mut self,
348 tree: &mut Tree,
349 renderer: &crate::Renderer,
350 limits: &iced_core::layout::Limits,
351 ) -> iced_core::layout::Node {
352 self.content
353 .as_widget_mut()
354 .layout(&mut tree.children[0], renderer, limits)
355 }
356
357 fn draw(
358 &self,
359 tree: &Tree,
360 renderer: &mut crate::Renderer,
361 theme: &crate::Theme,
362 style: &iced_core::renderer::Style,
363 layout: iced_core::Layout<'_>,
364 cursor: iced_core::mouse::Cursor,
365 viewport: &iced::Rectangle,
366 ) {
367 self.content.as_widget().draw(
368 &tree.children[0],
369 renderer,
370 theme,
371 style,
372 layout,
373 cursor,
374 viewport,
375 );
376 }
377
378 fn mouse_interaction(
379 &self,
380 tree: &Tree,
381 layout: iced_core::Layout<'_>,
382 cursor: iced_core::mouse::Cursor,
383 viewport: &iced::Rectangle,
384 renderer: &crate::Renderer,
385 ) -> mouse::Interaction {
386 self.content.as_widget().mouse_interaction(
387 &tree.children[0],
388 layout,
389 cursor,
390 viewport,
391 renderer,
392 )
393 }
394
395 fn drag_destinations(
396 &self,
397 tree: &Tree,
398 layout: iced_core::Layout<'_>,
399 renderer: &crate::Renderer,
400 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
401 ) {
402 self.content.as_widget().drag_destinations(
403 &tree.children[0],
404 layout,
405 renderer,
406 dnd_rectangles,
407 );
408 }
409
410 fn operate(
411 &mut self,
412 tree: &mut Tree,
413 layout: iced_core::Layout<'_>,
414 renderer: &crate::Renderer,
415 operation: &mut dyn iced_core::widget::Operation<()>,
416 ) {
417 self.content
418 .as_widget_mut()
419 .operate(&mut tree.children[0], layout, renderer, operation);
420 }
421
422 #[allow(clippy::too_many_lines)]
423 fn update(
424 &mut self,
425 tree: &mut Tree,
426 event: &iced::Event,
427 layout: iced_core::Layout<'_>,
428 cursor: iced_core::mouse::Cursor,
429 renderer: &crate::Renderer,
430 clipboard: &mut dyn iced_core::Clipboard,
431 shell: &mut iced_core::Shell<'_, Message>,
432 viewport: &iced::Rectangle,
433 ) {
434 let state = tree.state.downcast_mut::<LocalState<Message>>();
435 let bounds = layout.bounds();
436
437 #[cfg(wayland_platform)]
439 if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
440 iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup),
441 )) = event
442 {
443 state.menu_bar_state.inner.with_data_mut(|d| {
444 if d.popup_id.get(&self.window_id) == Some(popup) {
445 d.popup_id.remove(&self.window_id);
446 d.reset();
447 }
448 });
449 }
450
451 #[cfg(wayland_platform)]
453 if state.reshape {
454 state.reshape = false;
455 if state.menu_bar_state.inner.with_data(|d| d.open) {
456 if let Some(context_menu) = self.context_menu.as_mut() {
457 state.menu_bar_state.inner.with_data_mut(|inner| {
458 menu_roots_diff(context_menu, &mut inner.tree);
459 });
460 state
461 .roots
462 .with_data_mut(|roots| roots.clone_from(context_menu));
463 }
464 let view_cursor = state.menu_bar_state.inner.with_data(|d| d.view_cursor);
465 self.create_popup(layout, view_cursor, renderer, shell, viewport, state);
466 }
467 }
468
469 let reset = self.window_id != window::Id::NONE
471 && state
472 .menu_bar_state
473 .inner
474 .with_data(|d| !d.open && !d.active_root.is_empty());
475
476 let open = state.menu_bar_state.inner.with_data_mut(|state| {
477 if reset
478 && let Some(popup_id) = state.popup_id.get(&self.window_id).copied()
479 && let Some(handler) = self.on_surface_action.as_ref()
480 {
481 shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id)));
482 state.reset();
483 }
484 state.open
485 });
486 let mut was_open = false;
487 if matches!(event,
488 Event::Keyboard(keyboard::Event::KeyPressed {
489 key: keyboard::Key::Named(keyboard::key::Named::Escape),
490 ..
491 })
492 | Event::Mouse(mouse::Event::ButtonPressed(
493 mouse::Button::Right | mouse::Button::Left,
494 ))
495 | Event::Touch(touch::Event::FingerPressed { .. })
496 if open )
497 {
498 state.menu_bar_state.inner.with_data_mut(|state| {
499 was_open = true;
500 state.menu_states.clear();
501 state.active_root.clear();
502 state.open = false;
503
504 #[cfg(wayland_platform)]
505 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
506 && let Some(id) = state.popup_id.remove(&self.window_id)
507 {
508 {
509 let surface_action = self.on_surface_action.as_ref().unwrap();
510 shell.publish(surface_action(crate::surface::action::destroy_popup(id)));
511 }
512 state.view_cursor = cursor;
513 }
514 });
515 }
516
517 if !was_open && cursor.is_over(bounds) {
518 let fingers_pressed = state.fingers_pressed.len();
519
520 match event {
521 Event::Touch(touch::Event::FingerPressed { id, .. }) => {
522 state.fingers_pressed.insert(*id);
523 }
524
525 Event::Touch(touch::Event::FingerLifted { id, .. }) => {
526 state.fingers_pressed.remove(id);
527 }
528
529 _ => (),
530 }
531
532 if !was_open
534 && self.context_menu.is_some()
535 && (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2))
536 {
537 state.context_cursor = cursor.position().unwrap_or_default();
538 let state = tree.state.downcast_mut::<LocalState<Message>>();
539 state.menu_bar_state.inner.with_data_mut(|state| {
540 state.open = true;
541 state.view_cursor = cursor;
542 });
543 #[cfg(wayland_platform)]
544 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) {
545 self.create_popup(layout, cursor, renderer, shell, viewport, state);
546 }
547
548 shell.request_redraw();
549 shell.capture_event();
550 self.report_open_state(tree.state.downcast_mut::<LocalState<Message>>(), shell);
551 return;
552 } else if !was_open && right_button_released(event)
553 || (touch_lifted(event))
554 || left_button_released(event)
555 {
556 state.menu_bar_state.inner.with_data_mut(|state| {
557 was_open = true;
558 state.menu_states.clear();
559 state.active_root.clear();
560 state.open = false;
561
562 #[cfg(wayland_platform)]
563 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
564 && let Some(id) = state.popup_id.remove(&self.window_id)
565 {
566 {
567 let surface_action = self.on_surface_action.as_ref().unwrap();
568 shell
569 .publish(surface_action(crate::surface::action::destroy_popup(id)));
570 }
571 state.view_cursor = cursor;
572 }
573 });
574 }
575 }
576 self.content.as_widget_mut().update(
577 &mut tree.children[0],
578 event,
579 layout,
580 cursor,
581 renderer,
582 clipboard,
583 shell,
584 viewport,
585 );
586 self.report_open_state(tree.state.downcast_mut::<LocalState<Message>>(), shell);
587 }
588
589 fn overlay<'b>(
590 &'b mut self,
591 tree: &'b mut Tree,
592 layout: iced_core::Layout<'b>,
593 renderer: &crate::Renderer,
594 viewport: &iced::Rectangle,
595 translation: Vector,
596 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
597 let content = self.content.as_widget_mut().overlay(
599 &mut tree.children[0],
600 layout,
601 renderer,
602 viewport,
603 translation,
604 );
605
606 #[cfg(wayland_platform)]
607 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
608 && self.window_id != window::Id::NONE
609 && self.on_surface_action.is_some()
610 {
611 return content;
612 }
613
614 let state = tree.state.downcast_ref::<LocalState<Message>>();
615 let Some(context_menu) = self.context_menu.as_mut() else {
616 return content;
617 };
618 if !state.menu_bar_state.inner.with_data(|state| state.open) {
619 return content;
620 }
621
622 let bounds = iced::Rectangle::new(state.context_cursor, Size::new(1.0, 1.0));
624 let menu = crate::widget::menu::Menu {
625 tree: state.menu_bar_state.clone(),
626 menu_roots: std::borrow::Cow::Owned(context_menu.clone()),
627 bounds_expand: 16,
628 menu_overlays_parent: true,
629 close_condition: CloseCondition {
630 leave: false,
631 click_outside: true,
632 click_inside: true,
633 },
634 item_width: self.item_width,
635 item_height: ItemHeight::Dynamic(40),
636 bar_bounds: bounds,
637 main_offset: 0,
638 cross_offset: 0,
639 root_bounds_list: vec![bounds],
640 path_highlight: Some(PathHighlight::MenuActive),
641 style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
642 position: Point::new(translation.x, translation.y),
643 is_overlay: true,
644 window_id: window::Id::NONE,
645 depth: 0,
646 on_surface_action: None,
647 }
648 .overlay();
649
650 Some(match content {
651 Some(content) => {
652 iced_core::overlay::Group::with_children(vec![content, menu]).overlay()
653 }
654 None => menu,
655 })
656 }
657
658 #[cfg(feature = "a11y")]
659 fn a11y_nodes(
661 &self,
662 layout: iced_core::Layout<'_>,
663 state: &Tree,
664 p: mouse::Cursor,
665 ) -> iced_accessibility::A11yTree {
666 let c_state = &state.children[0];
667 self.content.as_widget().a11y_nodes(layout, c_state, p)
668 }
669}
670
671impl<'a, Message: Clone + 'static> From<ContextMenu<'a, Message>> for crate::Element<'a, Message> {
672 fn from(widget: ContextMenu<'a, Message>) -> Self {
673 Self::new(widget)
674 }
675}
676
677fn right_button_released(event: &Event) -> bool {
678 matches!(
679 event,
680 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right,))
681 )
682}
683
684fn left_button_released(event: &Event) -> bool {
685 matches!(
686 event,
687 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,))
688 )
689}
690
691fn touch_lifted(event: &Event) -> bool {
692 matches!(event, Event::Touch(touch::Event::FingerLifted { .. }))
693}
694
695pub struct LocalState<Message> {
696 context_cursor: Point,
697 fingers_pressed: HashSet<Finger>,
698 menu_bar_state: MenuBarState,
699 reported_open: bool,
700 roots: RcWrapper<Vec<menu::Tree<Message>>>,
701 reshape: bool,
703}