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