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