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