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