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