1use std::{collections::HashMap, sync::Arc};
5
6use super::{
7 menu_inner::{
8 CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight,
9 },
10 menu_tree::MenuTree,
11};
12#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
13use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem};
14use crate::{
15 Renderer,
16 style::menu_bar::StyleSheet,
17 widget::{
18 RcWrapper,
19 dropdown::menu::{self, State},
20 menu::menu_inner::init_root_menu,
21 },
22};
23
24use iced::{Point, Shadow, Vector, window};
25use iced_core::Border;
26use iced_widget::core::{
27 Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event,
28 layout::{Limits, Node},
29 mouse::{self, Cursor},
30 overlay,
31 renderer::{self, Renderer as IcedRenderer},
32 touch,
33 widget::{Tree, tree},
34};
35
36pub fn menu_bar<Message>(menu_roots: Vec<MenuTree<Message>>) -> MenuBar<Message>
38where
39 Message: Clone + 'static,
40{
41 MenuBar::new(menu_roots)
42}
43
44#[derive(Clone, Default)]
45pub(crate) struct MenuBarState {
46 pub(crate) inner: RcWrapper<MenuBarStateInner>,
47}
48
49pub(crate) struct MenuBarStateInner {
50 pub(crate) tree: Tree,
51 pub(crate) popup_id: HashMap<window::Id, window::Id>,
52 pub(crate) pressed: bool,
53 pub(crate) bar_pressed: bool,
54 pub(crate) view_cursor: Cursor,
55 pub(crate) open: bool,
56 pub(crate) active_root: Vec<usize>,
57 pub(crate) horizontal_direction: Direction,
58 pub(crate) vertical_direction: Direction,
59 pub(crate) menu_states: Vec<MenuState>,
61}
62impl MenuBarStateInner {
63 pub(super) fn get_trimmed_indices(&self, index: usize) -> impl Iterator<Item = usize> + '_ {
65 self.menu_states
66 .iter()
67 .skip(index)
68 .take_while(|ms| ms.index.is_some())
69 .map(|ms| ms.index.expect("No indices were found in the menu state."))
70 }
71
72 pub(crate) fn reset(&mut self) {
73 self.open = false;
74 self.active_root = Vec::new();
75 self.menu_states.clear();
76 }
77}
78impl Default for MenuBarStateInner {
79 fn default() -> Self {
80 Self {
81 tree: Tree::empty(),
82 pressed: false,
83 view_cursor: Cursor::Available([-0.5, -0.5].into()),
84 open: false,
85 active_root: Vec::new(),
86 horizontal_direction: Direction::Positive,
87 vertical_direction: Direction::Positive,
88 menu_states: Vec::new(),
89 popup_id: HashMap::new(),
90 bar_pressed: false,
91 }
92 }
93}
94
95pub(crate) fn menu_roots_children<Message>(menu_roots: &Vec<MenuTree<Message>>) -> Vec<Tree>
96where
97 Message: Clone + 'static,
98{
99 menu_roots
109 .iter()
110 .map(|root| {
111 let mut tree = Tree::empty();
112 let flat = root
113 .flattern()
114 .iter()
115 .map(|mt| Tree::new(mt.item.clone()))
116 .collect();
117 tree.children = flat;
118 tree
119 })
120 .collect()
121}
122
123#[allow(invalid_reference_casting)]
124pub(crate) fn menu_roots_diff<Message>(menu_roots: &mut Vec<MenuTree<Message>>, tree: &mut Tree)
125where
126 Message: Clone + 'static,
127{
128 if tree.children.len() > menu_roots.len() {
129 tree.children.truncate(menu_roots.len());
130 }
131
132 tree.children
133 .iter_mut()
134 .zip(menu_roots.iter())
135 .for_each(|(t, root)| {
136 let mut flat = root
137 .flattern()
138 .iter()
139 .map(|mt| {
140 let widget = &mt.item;
141 let widget_ptr = widget as *const dyn Widget<Message, crate::Theme, Renderer>;
142 let widget_ptr_mut =
143 widget_ptr as *mut dyn Widget<Message, crate::Theme, Renderer>;
144 unsafe { &mut *widget_ptr_mut }
146 })
147 .collect::<Vec<_>>();
148
149 t.diff_children(flat.as_mut_slice());
150 });
151
152 if tree.children.len() < menu_roots.len() {
153 let extended = menu_roots[tree.children.len()..].iter().map(|root| {
154 let mut tree = Tree::empty();
155 let flat = root
156 .flattern()
157 .iter()
158 .map(|mt| Tree::new(mt.item.clone()))
159 .collect();
160 tree.children = flat;
161 tree
162 });
163 tree.children.extend(extended);
164 }
165}
166
167pub fn get_mut_or_default<T: Default>(vec: &mut Vec<T>, index: usize) -> &mut T {
168 if index < vec.len() {
169 &mut vec[index]
170 } else {
171 vec.resize_with(index + 1, T::default);
172 &mut vec[index]
173 }
174}
175
176#[allow(missing_debug_implementations)]
178pub struct MenuBar<Message> {
179 width: Length,
180 height: Length,
181 spacing: f32,
182 padding: Padding,
183 bounds_expand: u16,
184 main_offset: i32,
185 cross_offset: i32,
186 close_condition: CloseCondition,
187 item_width: ItemWidth,
188 item_height: ItemHeight,
189 path_highlight: Option<PathHighlight>,
190 menu_roots: Vec<MenuTree<Message>>,
191 style: <crate::Theme as StyleSheet>::Style,
192 window_id: window::Id,
193 #[cfg(all(feature = "wayland", feature = "winit"))]
194 positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
195 pub(crate) on_surface_action:
196 Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
197}
198
199impl<Message> MenuBar<Message>
200where
201 Message: Clone + 'static,
202{
203 #[must_use]
205 pub fn new(menu_roots: Vec<MenuTree<Message>>) -> Self {
206 let mut menu_roots = menu_roots;
207 menu_roots.iter_mut().for_each(MenuTree::set_index);
208
209 Self {
210 width: Length::Shrink,
211 height: Length::Shrink,
212 spacing: 0.0,
213 padding: Padding::ZERO,
214 bounds_expand: 16,
215 main_offset: 0,
216 cross_offset: 0,
217 close_condition: CloseCondition {
218 leave: false,
219 click_outside: true,
220 click_inside: true,
221 },
222 item_width: ItemWidth::Uniform(150),
223 item_height: ItemHeight::Uniform(30),
224 path_highlight: Some(PathHighlight::MenuActive),
225 menu_roots,
226 style: <crate::Theme as StyleSheet>::Style::default(),
227 window_id: window::Id::NONE,
228 #[cfg(all(feature = "wayland", feature = "winit"))]
229 positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
230 on_surface_action: None,
231 }
232 }
233
234 #[must_use]
240 pub fn bounds_expand(mut self, value: u16) -> Self {
241 self.bounds_expand = value;
242 self
243 }
244
245 #[must_use]
247 pub fn close_condition(mut self, close_condition: CloseCondition) -> Self {
248 self.close_condition = close_condition;
249 self
250 }
251
252 #[must_use]
254 pub fn cross_offset(mut self, value: i32) -> Self {
255 self.cross_offset = value;
256 self
257 }
258
259 #[must_use]
261 pub fn height(mut self, height: Length) -> Self {
262 self.height = height;
263 self
264 }
265
266 #[must_use]
268 pub fn item_height(mut self, item_height: ItemHeight) -> Self {
269 self.item_height = item_height;
270 self
271 }
272
273 #[must_use]
275 pub fn item_width(mut self, item_width: ItemWidth) -> Self {
276 self.item_width = item_width;
277 self
278 }
279
280 #[must_use]
282 pub fn main_offset(mut self, value: i32) -> Self {
283 self.main_offset = value;
284 self
285 }
286
287 #[must_use]
289 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
290 self.padding = padding.into();
291 self
292 }
293
294 #[must_use]
296 pub fn path_highlight(mut self, path_highlight: Option<PathHighlight>) -> Self {
297 self.path_highlight = path_highlight;
298 self
299 }
300
301 #[must_use]
303 pub fn spacing(mut self, units: f32) -> Self {
304 self.spacing = units;
305 self
306 }
307
308 #[must_use]
310 pub fn style(mut self, style: impl Into<<crate::Theme as StyleSheet>::Style>) -> Self {
311 self.style = style.into();
312 self
313 }
314
315 #[must_use]
317 pub fn width(mut self, width: Length) -> Self {
318 self.width = width;
319 self
320 }
321
322 #[cfg(all(feature = "wayland", feature = "winit"))]
323 pub fn with_positioner(
324 mut self,
325 positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
326 ) -> Self {
327 self.positioner = positioner;
328 self
329 }
330
331 #[must_use]
332 pub fn window_id(mut self, id: window::Id) -> Self {
333 self.window_id = id;
334 self
335 }
336
337 #[must_use]
338 pub fn window_id_maybe(mut self, id: Option<window::Id>) -> Self {
339 if let Some(id) = id {
340 self.window_id = id;
341 }
342 self
343 }
344
345 #[must_use]
346 pub fn on_surface_action(
347 mut self,
348 handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static,
349 ) -> Self {
350 self.on_surface_action = Some(Arc::new(handler));
351 self
352 }
353
354 #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
355 #[allow(clippy::too_many_lines)]
356 fn create_popup(
357 &mut self,
358 layout: Layout<'_>,
359 view_cursor: Cursor,
360 renderer: &Renderer,
361 shell: &mut Shell<'_, Message>,
362 viewport: &Rectangle,
363 my_state: &mut MenuBarState,
364 ) {
365 if self.window_id != window::Id::NONE && self.on_surface_action.is_some() {
366 use crate::surface::action::destroy_popup;
367 use iced_runtime::platform_specific::wayland::popup::{
368 SctkPopupSettings, SctkPositioner,
369 };
370
371 let surface_action = self.on_surface_action.as_ref().unwrap();
372 let old_active_root = my_state
373 .inner
374 .with_data(|state| state.active_root.get(0).copied());
375
376 let hovered_root = layout
378 .children()
379 .position(|lo| view_cursor.is_over(lo.bounds()));
380 if hovered_root.is_none()
381 || old_active_root
382 .zip(hovered_root)
383 .is_some_and(|r| r.0 == r.1)
384 {
385 return;
386 }
387
388 let (id, root_list) = my_state.inner.with_data_mut(|state| {
389 if let Some(id) = state.popup_id.get(&self.window_id).copied() {
390 state.menu_states.clear();
392 state.active_root.clear();
393 shell.publish(surface_action(destroy_popup(id)));
394 state.view_cursor = view_cursor;
395 (id, layout.children().map(|lo| lo.bounds()).collect())
396 } else {
397 (
398 window::Id::unique(),
399 layout.children().map(|lo| lo.bounds()).collect(),
400 )
401 }
402 });
403
404 let mut popup_menu: Menu<'static, _> = Menu {
405 tree: my_state.clone(),
406 menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()),
407 bounds_expand: self.bounds_expand,
408 menu_overlays_parent: false,
409 close_condition: self.close_condition,
410 item_width: self.item_width,
411 item_height: self.item_height,
412 bar_bounds: layout.bounds(),
413 main_offset: self.main_offset,
414 cross_offset: self.cross_offset,
415 root_bounds_list: root_list,
416 path_highlight: self.path_highlight,
417 style: std::borrow::Cow::Owned(self.style.clone()),
418 position: Point::new(0., 0.),
419 is_overlay: false,
420 window_id: id,
421 depth: 0,
422 on_surface_action: self.on_surface_action.clone(),
423 };
424
425 init_root_menu(
426 &mut popup_menu,
427 renderer,
428 shell,
429 view_cursor.position().unwrap(),
430 viewport.size(),
431 Vector::new(0., 0.),
432 layout.bounds(),
433 self.main_offset as f32,
434 );
435 let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| {
436 state.popup_id.insert(self.window_id, id);
437 (state
438 .menu_states
439 .iter()
440 .find(|s| s.index.is_none())
441 .map(|s| s.menu_bounds.parent_bounds)
442 .map_or_else(
443 || {
444 let bounds = layout.bounds();
445 Rectangle {
446 x: bounds.x as i32,
447 y: bounds.y as i32,
448 width: bounds.width as i32,
449 height: bounds.height as i32,
450 }
451 },
452 |r| Rectangle {
453 x: r.x as i32,
454 y: r.y as i32,
455 width: r.width as i32,
456 height: r.height as i32,
457 },
458 ), match (state.horizontal_direction, state.vertical_direction) {
459 (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
460 (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
461 (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
462 (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
463 })
464 });
465
466 let menu_node = popup_menu.layout(renderer, Limits::NONE.min_width(1.).min_height(1.));
467 let popup_size = menu_node.size();
468 let positioner = SctkPositioner {
469 size: Some((
470 popup_size.width.ceil() as u32 + 2,
471 popup_size.height.ceil() as u32 + 2,
472 )),
473 anchor_rect,
474 anchor:
475 cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
476 gravity,
477 reactive: true,
478 ..Default::default()
479 };
480 let parent = self.window_id;
481 shell.publish((surface_action)(crate::surface::action::simple_popup(
482 move || SctkPopupSettings {
483 parent,
484 id,
485 positioner: positioner.clone(),
486 parent_size: None,
487 grab: true,
488 close_with_children: false,
489 input_zone: None,
490 },
491 Some(move || {
492 Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill))
493 .map(crate::action::app)
494 }),
495 )));
496 }
497 }
498}
499impl<Message> Widget<Message, crate::Theme, Renderer> for MenuBar<Message>
500where
501 Message: Clone + 'static,
502{
503 fn size(&self) -> iced_core::Size<Length> {
504 iced_core::Size::new(self.width, self.height)
505 }
506
507 fn diff(&mut self, tree: &mut Tree) {
508 let state = tree.state.downcast_mut::<MenuBarState>();
509 state
510 .inner
511 .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree));
512 }
513
514 fn tag(&self) -> tree::Tag {
515 tree::Tag::of::<MenuBarState>()
516 }
517
518 fn state(&self) -> tree::State {
519 tree::State::new(MenuBarState::default())
520 }
521
522 fn children(&self) -> Vec<Tree> {
523 menu_roots_children(&self.menu_roots)
524 }
525
526 fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
527 use super::flex;
528
529 let limits = limits.width(self.width).height(self.height);
530 let children = self
531 .menu_roots
532 .iter()
533 .map(|root| &root.item)
534 .collect::<Vec<_>>();
535 let mut tree_children = tree
537 .children
538 .iter_mut()
539 .map(|t| &mut t.children[0])
540 .collect::<Vec<_>>();
541 flex::resolve_wrapper(
542 &flex::Axis::Horizontal,
543 renderer,
544 &limits,
545 self.padding,
546 self.spacing,
547 Alignment::Center,
548 &children,
549 &mut tree_children,
550 )
551 }
552
553 #[allow(clippy::too_many_lines)]
554 fn on_event(
555 &mut self,
556 tree: &mut Tree,
557 event: event::Event,
558 layout: Layout<'_>,
559 view_cursor: Cursor,
560 renderer: &Renderer,
561 clipboard: &mut dyn Clipboard,
562 shell: &mut Shell<'_, Message>,
563 viewport: &Rectangle,
564 ) -> event::Status {
565 use event::Event::{Mouse, Touch};
566 use mouse::{Button::Left, Event::ButtonReleased};
567 use touch::Event::{FingerLifted, FingerLost};
568
569 let root_status = process_root_events(
570 &mut self.menu_roots,
571 view_cursor,
572 tree,
573 &event,
574 layout,
575 renderer,
576 clipboard,
577 shell,
578 viewport,
579 );
580
581 let my_state = tree.state.downcast_mut::<MenuBarState>();
582
583 let reset = self.window_id != window::Id::NONE
585 && my_state
586 .inner
587 .with_data(|d| !d.open && !d.active_root.is_empty());
588
589 let open = my_state.inner.with_data_mut(|state| {
590 if reset {
591 if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() {
592 if let Some(handler) = self.on_surface_action.as_ref() {
593 shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id)));
594 state.reset();
595 }
596 }
597 }
598 state.open
599 });
600
601 match event {
602 Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => {
603 let create_popup = my_state.inner.with_data_mut(|state| {
604 let mut create_popup = false;
605 if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
606 state.view_cursor = view_cursor;
607 state.open = true;
608 create_popup = true;
609 } else if let Some(_id) = state.popup_id.remove(&self.window_id) {
610 state.menu_states.clear();
611 state.active_root.clear();
612 state.open = false;
613 #[cfg(all(
614 feature = "wayland",
615 feature = "winit",
616 feature = "surface-message"
617 ))]
618 {
619 let surface_action = self.on_surface_action.as_ref().unwrap();
620
621 shell.publish(surface_action(crate::surface::action::destroy_popup(
622 _id,
623 )));
624 }
625 state.view_cursor = view_cursor;
626 }
627 create_popup
628 });
629
630 if !create_popup {
631 return event::Status::Ignored;
632 }
633 #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
634 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) {
635 self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state);
636 }
637 }
638 Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered)
639 if open && view_cursor.is_over(layout.bounds()) =>
640 {
641 #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
642 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) {
643 self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state);
644 }
645 }
646 _ => (),
647 }
648
649 root_status
650 }
651
652 fn draw(
653 &self,
654 tree: &Tree,
655 renderer: &mut Renderer,
656 theme: &crate::Theme,
657 style: &renderer::Style,
658 layout: Layout<'_>,
659 view_cursor: Cursor,
660 viewport: &Rectangle,
661 ) {
662 let state = tree.state.downcast_ref::<MenuBarState>();
663 let cursor_pos = view_cursor.position().unwrap_or_default();
664 state.inner.with_data_mut(|state| {
665 let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) {
666 state.view_cursor
667 } else {
668 view_cursor
669 };
670
671 if self.path_highlight.is_some() {
673 let styling = theme.appearance(&self.style);
674 if let Some(active) = state.active_root.first() {
675 let active_bounds = layout
676 .children()
677 .nth(*active)
678 .expect("Active child not found in menu?")
679 .bounds();
680 let path_quad = renderer::Quad {
681 bounds: active_bounds,
682 border: Border {
683 radius: styling.bar_border_radius.into(),
684 ..Default::default()
685 },
686 shadow: Shadow::default(),
687 };
688
689 renderer.fill_quad(path_quad, styling.path);
690 }
691 }
692
693 self.menu_roots
694 .iter()
695 .zip(&tree.children)
696 .zip(layout.children())
697 .for_each(|((root, t), lo)| {
698 root.item.draw(
699 &t.children[root.index],
700 renderer,
701 theme,
702 style,
703 lo,
704 position,
705 viewport,
706 );
707 });
708 });
709 }
710
711 fn overlay<'b>(
712 &'b mut self,
713 tree: &'b mut Tree,
714 layout: Layout<'_>,
715 _renderer: &Renderer,
716 translation: Vector,
717 ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
718 #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))]
719 if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
720 && self.on_surface_action.is_some()
721 && self.window_id != window::Id::NONE
722 {
723 return None;
724 }
725
726 let state = tree.state.downcast_ref::<MenuBarState>();
727 if state.inner.with_data(|state| !state.open) {
728 return None;
729 }
730
731 Some(
732 Menu {
733 tree: state.clone(),
734 menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()),
735 bounds_expand: self.bounds_expand,
736 menu_overlays_parent: false,
737 close_condition: self.close_condition,
738 item_width: self.item_width,
739 item_height: self.item_height,
740 bar_bounds: layout.bounds(),
741 main_offset: self.main_offset,
742 cross_offset: self.cross_offset,
743 root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(),
744 path_highlight: self.path_highlight,
745 style: std::borrow::Cow::Borrowed(&self.style),
746 position: Point::new(translation.x, translation.y),
747 is_overlay: true,
748 window_id: window::Id::NONE,
749 depth: 0,
750 on_surface_action: self.on_surface_action.clone(),
751 }
752 .overlay(),
753 )
754 }
755}
756
757impl<Message> From<MenuBar<Message>> for Element<'_, Message, crate::Theme, Renderer>
758where
759 Message: Clone + 'static,
760{
761 fn from(value: MenuBar<Message>) -> Self {
762 Self::new(value)
763 }
764}
765
766#[allow(unused_results, clippy::too_many_arguments)]
767fn process_root_events<Message>(
768 menu_roots: &mut [MenuTree<Message>],
769 view_cursor: Cursor,
770 tree: &mut Tree,
771 event: &event::Event,
772 layout: Layout<'_>,
773 renderer: &Renderer,
774 clipboard: &mut dyn Clipboard,
775 shell: &mut Shell<'_, Message>,
776 viewport: &Rectangle,
777) -> event::Status
778where
779{
780 menu_roots
781 .iter_mut()
782 .zip(&mut tree.children)
783 .zip(layout.children())
784 .map(|((root, t), lo)| {
785 root.item.on_event(
787 &mut t.children[root.index],
788 event.clone(),
789 lo,
790 view_cursor,
791 renderer,
792 clipboard,
793 shell,
794 viewport,
795 )
796 })
797 .fold(event::Status::Ignored, event::Status::merge)
798}