1#![allow(clippy::unwrap_used)]
4#![allow(clippy::doc_markdown)]
5#![allow(clippy::wildcard_imports)]
6#![allow(clippy::enum_glob_use)]
7
8use iced_core::{
9 Clipboard, Element, Event, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget,
10 alignment, event,
11 layout::{Limits, Node},
12 mouse, overlay, renderer,
13 widget::{Operation, Tree, tree},
14 window,
15};
16
17use super::{common::*, flex, menu_bar_overlay::MenuBarOverlay, menu_tree::*};
18use crate::style::menu_bar::*;
19pub use crate::style::status::{Status, StyleFn};
20
21#[cfg(feature = "debug_log")]
22use log::debug;
23
24#[derive(Debug, Clone, Copy)]
25pub(super) enum MenuBarTask {
26 OpenOnClick,
27 CloseOnClick,
28}
29
30#[derive(Default, Debug)]
31pub(super) struct GlobalState {
32 pub(super) open: bool,
33 pub(super) pressed: bool,
34 task: Option<MenuBarTask>,
35}
36impl GlobalState {
37 pub(super) fn schedule(&mut self, task: MenuBarTask) {
38 self.task = Some(task);
39 }
40
41 pub(super) fn task(&self) -> Option<MenuBarTask> {
42 self.task
43 }
44
45 pub(super) fn clear_task(&mut self) {
46 self.task = None;
47 }
48}
49
50#[derive(Default, Debug)]
51pub(super) struct MenuBarState {
52 pub(super) global_state: GlobalState,
53 pub(super) menu_state: MenuState,
54}
55impl MenuBarState {
56 pub(super) fn open<'a, 'b, Message, Theme: Catalog, Renderer: renderer::Renderer>(
57 &mut self,
58 roots: &mut [Item<'a, Message, Theme, Renderer>],
59 item_trees: &mut [Tree],
60 item_layouts: impl Iterator<Item = Layout<'b>>,
61 cursor: mouse::Cursor,
62 shell: &mut Shell<'_, Message>,
63 ) {
64 if !self.global_state.open {
65 self.global_state.open = true;
66 self.menu_state.active = None;
67 }
68
69 try_open_menu(
70 roots,
71 &mut self.menu_state,
72 item_trees,
73 item_layouts,
74 cursor,
75 shell,
76 );
77
78 self.global_state.task = None;
79 }
80
81 pub(super) fn close<Message>(
82 &mut self,
83 item_trees: &mut [Tree],
84 shell: &mut Shell<'_, Message>,
85 ) {
86 if self.global_state.pressed {
87 return;
88 }
89
90 for item_tree in item_trees.iter_mut() {
91 if item_tree.children.len() == 2 {
92 let _ = item_tree.children.pop();
93 shell.invalidate_layout();
94 }
95 }
96 self.global_state.pressed = false;
97 self.global_state.task = None;
98 self.global_state.open = false;
99 self.menu_state.active = None;
100 shell.request_redraw();
101 }
102}
103
104#[must_use]
106pub struct MenuBar<'a, Message, Theme, Renderer>
107where
108 Theme: Catalog,
109 Renderer: renderer::Renderer,
110{
111 pub(super) roots: Vec<Item<'a, Message, Theme, Renderer>>,
112 spacing: Pixels,
113 padding: Padding,
114 width: Length,
115 height: Length,
116 close_on_item_click: Option<bool>,
117 close_on_background_click: Option<bool>,
118 pub(super) global_parameters: GlobalParameters<'a, Theme>,
119}
120impl<'a, Message, Theme, Renderer> MenuBar<'a, Message, Theme, Renderer>
121where
122 Theme: Catalog,
123 Renderer: renderer::Renderer,
124{
125 pub fn new(mut roots: Vec<Item<'a, Message, Theme, Renderer>>) -> Self {
127 for i in &mut roots {
128 if let Some(m) = i.menu.as_mut() {
129 m.axis = Axis::Vertical;
130 }
131 }
132
133 Self {
134 roots,
135 spacing: Pixels::ZERO,
136 padding: Padding::ZERO,
137 width: Length::Shrink,
138 height: Length::Shrink,
139 close_on_item_click: None,
140 close_on_background_click: None,
141 global_parameters: GlobalParameters {
142 safe_bounds_margin: 50.0,
143 draw_path: DrawPath::FakeHovering,
144 scroll_speed: ScrollSpeed {
145 line: 60.0,
146 pixel: 1.0,
147 },
148 close_on_item_click: false,
149 close_on_background_click: false,
150 class: Theme::default(),
151 },
152 }
153 }
154
155 pub fn width(mut self, width: impl Into<Length>) -> Self {
157 self.width = width.into();
158 self
159 }
160
161 pub fn height(mut self, height: impl Into<Length>) -> Self {
163 self.height = height.into();
164 self
165 }
166
167 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
169 self.spacing = spacing.into();
170 self
171 }
172
173 pub fn safe_bounds_margin(mut self, margin: f32) -> Self {
178 self.global_parameters.safe_bounds_margin = margin;
179 self
180 }
181
182 pub fn draw_path(mut self, draw_path: DrawPath) -> Self {
184 self.global_parameters.draw_path = draw_path;
185 self
186 }
187
188 pub fn scroll_speed(mut self, scroll_speed: ScrollSpeed) -> Self {
190 self.global_parameters.scroll_speed = scroll_speed;
191 self
192 }
193
194 pub fn close_on_item_click(mut self, value: bool) -> Self {
196 self.close_on_item_click = Some(value);
197 self
198 }
199
200 pub fn close_on_background_click(mut self, value: bool) -> Self {
202 self.close_on_background_click = Some(value);
203 self
204 }
205
206 pub fn close_on_item_click_global(mut self, value: bool) -> Self {
208 self.global_parameters.close_on_item_click = value;
209 self
210 }
211
212 pub fn close_on_background_click_global(mut self, value: bool) -> Self {
214 self.global_parameters.close_on_background_click = value;
215 self
216 }
217
218 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
220 self.padding = padding.into();
221 self
222 }
223
224 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
226 where
227 Theme::Class<'a>: From<StyleFn<'a, Theme, Style>>,
228 {
229 self.global_parameters.class = (Box::new(style) as StyleFn<'a, Theme, Style>).into();
230 self
231 }
232
233 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
235 self.global_parameters.class = class.into();
236 self
237 }
238}
239impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
240 for MenuBar<'_, Message, Theme, Renderer>
241where
242 Theme: Catalog,
243 Renderer: renderer::Renderer,
244{
245 fn size(&self) -> Size<Length> {
246 Size::new(self.width, self.height)
247 }
248
249 fn tag(&self) -> tree::Tag {
250 tree::Tag::of::<MenuBarState>()
251 }
252
253 fn state(&self) -> tree::State {
254 tree::State::Some(Box::<MenuBarState>::default())
255 }
256
257 fn children(&self) -> Vec<Tree> {
259 self.roots.iter().map(Item::tree).collect::<Vec<_>>()
260 }
261
262 fn diff(&self, tree: &mut Tree) {
264 tree.diff_children_custom(&self.roots, |tree, item| item.diff(tree), Item::tree);
265 }
266
267 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
271 let MenuBarState {
274 menu_state: bar_menu_state,
275 ..
276 } = tree.state.downcast_mut::<MenuBarState>();
277
278 let items_node = flex::resolve(
279 flex::Axis::Horizontal,
280 renderer,
281 &Limits::new(
282 Size {
283 width: 0.0,
284 height: limits.min().height,
285 },
286 Size {
287 width: f32::INFINITY,
288 height: limits.max().height,
289 },
290 ),
291 Length::Shrink,
292 self.height,
294 self.padding,
295 self.spacing,
296 alignment::Alignment::Center,
297 &mut self
298 .roots
299 .iter_mut()
300 .map(|item| &mut item.item)
301 .collect::<Vec<_>>(),
302 &mut tree
303 .children
304 .iter_mut()
305 .map(|tree| &mut tree.children[0])
306 .collect::<Vec<_>>(),
307 );
308
309 let items_node_bounds = items_node.bounds();
310 #[cfg(feature = "debug_log")]
311 debug!(
312 "menu::MenuBar::layout | items_node_bounds: {:?}",
313 items_node_bounds
314 );
315
316 let resolved_width = match self.width {
317 Length::Fill | Length::FillPortion(_) => items_node_bounds
318 .width
319 .min(limits.max().width)
320 .max(limits.min().width),
321 Length::Fixed(amount) => amount.min(limits.max().width).max(limits.min().width),
322 Length::Shrink => items_node_bounds.width,
323 };
324
325 let lower_bound_rel = self.padding.left - bar_menu_state.scroll_offset;
326 let upper_bound_rel = lower_bound_rel + resolved_width - self.padding.x();
327
328 let slice =
329 MenuSlice::from_bounds_rel(lower_bound_rel, upper_bound_rel, &items_node, |n| {
330 n.bounds().x
331 });
332 #[cfg(feature = "debug_log")]
333 debug!("menu::MenuBar::layout | slice: {:?}", slice);
334
335 bar_menu_state.slice = slice;
336
337 let slice_node = if slice.start_index == slice.end_index {
338 let node = &items_node.children()[slice.start_index];
339 let bounds = node.bounds();
340 let start_offset = slice.lower_bound_rel - bounds.x;
341 let width = slice.upper_bound_rel - slice.lower_bound_rel;
342
343 Node::with_children(
344 Size::new(width, items_node.bounds().height),
345 std::iter::once(clip_node_x(node, width, start_offset)).collect(),
346 )
347 } else {
348 let start_node = {
349 let node = &items_node.children()[slice.start_index];
350 let bounds = node.bounds();
351 let start_offset = slice.lower_bound_rel - bounds.x;
352 let width = bounds.width - start_offset;
353 clip_node_x(node, width, start_offset)
354 };
355
356 let end_node = {
357 let node = &items_node.children()[slice.end_index];
358 let bounds = node.bounds();
359 let width = slice.upper_bound_rel - bounds.x;
360 clip_node_x(node, width, 0.0)
361 };
362
363 Node::with_children(
364 items_node_bounds.size(),
365 std::iter::once(start_node)
366 .chain(
367 items_node.children()[slice.start_index + 1..slice.end_index]
368 .iter()
369 .map(Clone::clone),
370 )
371 .chain(std::iter::once(end_node))
372 .collect(),
373 )
374 };
375
376 Node::with_children(
377 Size {
378 width: resolved_width,
379 height: items_node_bounds.height,
380 },
381 [
382 slice_node.translate([bar_menu_state.scroll_offset, 0.0]),
384 ]
385 .into(),
386 )
387 }
388
389 fn update(
390 &mut self,
391 tree: &mut Tree,
392 event: &event::Event,
393 layout: Layout<'_>,
394 cursor: mouse::Cursor,
395 renderer: &Renderer,
396 clipboard: &mut dyn Clipboard,
397 shell: &mut Shell<'_, Message>,
398 viewport: &Rectangle,
399 ) {
400 #[cfg(feature = "debug_log")]
401 debug!(target:"menu::MenuBar::update", "");
402
403 let slice_layout = layout.children().next().unwrap();
404
405 let Tree {
406 state,
407 children: item_trees,
408 ..
409 } = tree;
410 let bar = state.downcast_mut::<MenuBarState>();
411 let MenuBarState {
412 global_state,
413 menu_state: bar_menu_state,
414 } = bar;
415
416 let slice = bar_menu_state.slice;
417 itl_iter_slice!(
418 slice,
419 self.roots;iter_mut,
420 item_trees;iter_mut,
421 slice_layout.children()
422 )
423 .for_each(|((item, tree), layout)| {
424 item.update(
425 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
426 );
427 });
428
429 let bar_bounds = layout.bounds();
430 match event {
435 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
436 if cursor.is_over(bar_bounds) {
437 global_state.pressed = true;
438 if global_state.open {
439 schedule_close_on_click(
440 global_state,
441 &self.global_parameters,
442 slice,
443 &mut self.roots,
444 slice_layout.children(),
445 cursor,
446 self.close_on_item_click,
447 self.close_on_background_click,
448 );
449 } else {
450 global_state.schedule(MenuBarTask::OpenOnClick);
451 }
452 shell.capture_event();
453 }
454 }
455 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
456 global_state.pressed = false;
457
458 if let Some(task) = global_state.task {
459 match task {
460 MenuBarTask::OpenOnClick => {
461 bar.open(
462 &mut self.roots,
463 item_trees,
464 slice_layout.children(),
465 cursor,
466 shell,
467 );
468 }
469 MenuBarTask::CloseOnClick => {
470 bar.close(item_trees, shell);
471 }
472 }
473 }
474 }
475 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
476 if global_state.open {
477 if cursor.is_over(bar_bounds) {
478 try_open_menu(
479 &mut self.roots,
480 bar_menu_state,
481 item_trees,
482 slice_layout.children(),
483 cursor,
484 shell,
485 );
486 shell.capture_event();
487 } else {
488 bar.close(item_trees, shell);
489 }
490 }
491 }
492 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
493 if cursor.is_over(bar_bounds) && slice_layout.bounds().width > layout.bounds().width
494 {
496 let scroll_speed = self.global_parameters.scroll_speed;
497 let delta_x = match delta {
498 mouse::ScrollDelta::Lines { x, .. } => x * scroll_speed.line,
499 mouse::ScrollDelta::Pixels { x, .. } => x * scroll_speed.pixel,
500 };
501
502 let min_offset = -(slice_layout.bounds().width - layout.bounds().width);
503
504 bar_menu_state.scroll_offset =
505 (bar_menu_state.scroll_offset + delta_x).clamp(min_offset, 0.0);
506 shell.invalidate_layout();
507 shell.request_redraw();
508 shell.capture_event();
509 }
510 }
511 Event::Window(window::Event::Resized { .. }) => {
512 if slice_layout.bounds().width > layout.bounds().width {
513 let min_offset = -(slice_layout.bounds().width - layout.bounds().width);
514
515 bar_menu_state.scroll_offset =
516 bar_menu_state.scroll_offset.clamp(min_offset, 0.0);
517 }
518 shell.invalidate_layout();
519 shell.request_redraw();
520 }
521 _ => {}
522 }
523
524 #[cfg(feature = "debug_log")]
525 debug!(target:"menu::MenuBar::update", "return | bar: {:?}", bar);
526 }
527
528 fn operate(
529 &mut self,
530 tree: &mut Tree,
531 layout: Layout<'_>,
532 renderer: &Renderer,
533 operation: &mut dyn Operation<()>,
534 ) {
535 let slice_layout = layout.children().next().unwrap();
536
537 let MenuBarState {
538 menu_state: bar_menu_state,
539 ..
540 } = tree.state.downcast_ref::<MenuBarState>();
541
542 let slice = bar_menu_state.slice;
543
544 operation.container(None, layout.bounds());
545 operation.traverse(&mut |operation| {
546 itl_iter_slice!(slice, self.roots;iter_mut, tree.children;iter_mut, slice_layout.children())
547 .for_each(|((child, state), layout)| {
548 child.operate(state, layout, renderer, operation);
549 });
550 });
551 }
552
553 fn mouse_interaction(
554 &self,
555 tree: &Tree,
556 layout: Layout<'_>,
557 cursor: mouse::Cursor,
558 _viewport: &Rectangle,
559 renderer: &Renderer,
560 ) -> mouse::Interaction {
561 let slice_layout = layout.children().next().unwrap();
562
563 let MenuBarState {
564 menu_state: bar_menu_state,
565 ..
566 } = tree.state.downcast_ref::<MenuBarState>();
567
568 itl_iter_slice!(bar_menu_state.slice, self.roots;iter, tree.children;iter, slice_layout.children())
569 .map(|((item, tree), layout)| item.mouse_interaction(tree, layout, cursor, renderer))
570 .max()
571 .unwrap_or_default()
572 }
573
574 fn draw(
575 &self,
576 tree: &Tree,
577 renderer: &mut Renderer,
578 theme: &Theme,
579 style: &renderer::Style,
580 layout: Layout<'_>,
581 cursor: mouse::Cursor,
582 viewport: &Rectangle,
583 ) {
584 let slice_layout = layout.children().next().unwrap();
585
586 let MenuBarState {
587 global_state,
588 menu_state: bar_menu_state,
589 } = tree.state.downcast_ref::<MenuBarState>();
590
591 let slice = bar_menu_state.slice;
592
593 let styling = theme.style(&self.global_parameters.class, Status::Active);
594 renderer.fill_quad(
595 renderer::Quad {
596 bounds: layout.bounds(),
597 border: styling.bar_border,
598 shadow: styling.bar_shadow,
599 ..Default::default()
600 },
601 styling.bar_background,
602 );
603
604 if let (DrawPath::Backdrop, true, Some(active)) = (
605 &self.global_parameters.draw_path,
606 global_state.open,
607 bar_menu_state.active,
608 ) {
609 let active_in_slice = active - slice.start_index;
610 let active_bounds = slice_layout
611 .children()
612 .nth(active_in_slice)
613 .unwrap_or_else(|| {
614 panic!(
615 "Index {:?} (in slice space) is not within the menu bar layout \
616 | slice_layout.children().count(): {:?} \
617 | This should not happen, please report this issue
618 ",
619 active_in_slice,
620 slice_layout.children().count()
621 )
622 })
623 .bounds();
624
625 renderer.fill_quad(
626 renderer::Quad {
627 bounds: active_bounds,
628 border: styling.path_border,
629 ..Default::default()
630 },
631 styling.path,
632 );
633 }
634
635 renderer.with_layer(
636 Rectangle {
637 x: layout.bounds().x + self.padding.left,
638 y: layout.bounds().y + self.padding.top,
639 width: layout.bounds().width - self.padding.x(),
640 height: layout.bounds().height - self.padding.y(),
641 },
642 |r| {
643 itl_iter_slice!(slice, self.roots;iter, tree.children;iter, slice_layout.children())
644 .for_each(|((item, tree), layout)| {
645 item.draw(tree, r, theme, style, layout, cursor, viewport);
646 });
647 },
648 );
649 }
650
651 fn overlay<'b>(
652 &'b mut self,
653 tree: &'b mut Tree,
654 layout: Layout<'b>,
655 renderer: &Renderer,
656 viewport: &Rectangle,
657 translation: iced_core::Vector,
658 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
659 #[cfg(feature = "debug_log")]
660 debug!(target:"menu::MenuBar::overlay", "");
661 let bar = tree.state.downcast_mut::<MenuBarState>();
662
663 if bar.global_state.open {
664 #[cfg(feature = "debug_log")]
665 debug!(target:"menu::MenuBar::overlay", "return | Menu Overlay");
666 Some(
667 MenuBarOverlay {
668 menu_bar: self,
669 layout,
670 translation,
671 tree,
672 }
673 .overlay_element(),
674 )
675 } else {
676 #[cfg(feature = "debug_log")]
677 debug!(target:"menu::MenuBar::overlay", "state not open | try return root overlays");
678 let slice_layout = layout.children().next()?;
679
680 let Tree {
681 state,
682 children: item_trees,
683 ..
684 } = tree;
685 let bar = state.downcast_mut::<MenuBarState>();
686 let MenuBarState {
687 menu_state: bar_menu_state,
688 ..
689 } = bar;
690
691 let slice = bar_menu_state.slice;
692
693 let overlays = itl_iter_slice!(slice, self.roots;iter_mut, item_trees;iter_mut, slice_layout.children())
694 .filter_map(|((item, item_tree), item_layout)| {
695 item.item.as_widget_mut().overlay(
696 &mut item_tree.children[0],
697 item_layout,
698 renderer,
699 viewport,
700 translation,
701 )
702 })
703 .collect::<Vec<_>>();
704
705 if overlays.is_empty() {
706 #[cfg(feature = "debug_log")]
707 debug!(target:"menu::MenuBar::overlay", "return | None");
708 None
709 } else {
710 #[cfg(feature = "debug_log")]
711 debug!(target:"menu::MenuBar::overlay", "return | Root Item Overlay");
712 Some(overlay::Group::with_children(overlays).overlay())
713 }
714 }
715 }
716}
717impl<'a, Message, Theme, Renderer> From<MenuBar<'a, Message, Theme, Renderer>>
718 for Element<'a, Message, Theme, Renderer>
719where
720 Message: 'a,
721 Theme: 'a + Catalog,
722 Renderer: 'a + renderer::Renderer,
723{
724 fn from(value: MenuBar<'a, Message, Theme, Renderer>) -> Self {
725 Self::new(value)
726 }
727}