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