iced_aw/widget/menu/
menu_bar.rs

1//! [`MenuBar`]
2
3#![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/// menu bar
105#[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    /// Creates a [`MenuBar`] with the given root items.
126    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    /// Sets the width of the [`MenuBar`].
156    pub fn width(mut self, width: impl Into<Length>) -> Self {
157        self.width = width.into();
158        self
159    }
160
161    /// Sets the height of the [`MenuBar`].
162    pub fn height(mut self, height: impl Into<Length>) -> Self {
163        self.height = height.into();
164        self
165    }
166
167    /// Sets the spacing of the [`MenuBar`].
168    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
169        self.spacing = spacing.into();
170        self
171    }
172
173    /// Sets the margin of the safe bounds of the [`Menu`]s in the [`MenuBar`].
174    ///
175    /// Defines a rectangular safe area that extends each menu's background bounds by a margin.
176    /// If the cursor moves outside this area, the menu will be closed.
177    pub fn safe_bounds_margin(mut self, margin: f32) -> Self {
178        self.global_parameters.safe_bounds_margin = margin;
179        self
180    }
181
182    /// Sets the draw path option of the [`MenuBar`]
183    pub fn draw_path(mut self, draw_path: DrawPath) -> Self {
184        self.global_parameters.draw_path = draw_path;
185        self
186    }
187
188    /// Sets the scroll speed of the [`Menu`]s in the [`MenuBar`]
189    pub fn scroll_speed(mut self, scroll_speed: ScrollSpeed) -> Self {
190        self.global_parameters.scroll_speed = scroll_speed;
191        self
192    }
193
194    /// Sets the close on item click option of the [`MenuBar`]
195    pub fn close_on_item_click(mut self, value: bool) -> Self {
196        self.close_on_item_click = Some(value);
197        self
198    }
199
200    /// Sets the close on background click option of the [`MenuBar`]
201    pub fn close_on_background_click(mut self, value: bool) -> Self {
202        self.close_on_background_click = Some(value);
203        self
204    }
205
206    /// Sets the global default close on item click option
207    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    /// Sets the global default close on background click option
213    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    /// Sets the padding of the [`MenuBar`].
219    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
220        self.padding = padding.into();
221        self
222    }
223
224    /// Sets the style of the [`MenuBar`].
225    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    /// Sets the class of the input of the [`MenuBar`].
234    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    /// \[Tree{stateless, \[widget_state, menu_state]}...]
258    fn children(&self) -> Vec<Tree> {
259        self.roots.iter().map(Item::tree).collect::<Vec<_>>()
260    }
261
262    /// tree: Tree{bar, \[item_tree...]}
263    fn diff(&self, tree: &mut Tree) {
264        tree.diff_children_custom(&self.roots, |tree, item| item.diff(tree), Item::tree);
265    }
266
267    /// tree: Tree{bar, \[item_tree...]}
268    ///
269    /// out: Node{bar bounds , \[widget_layout, widget_layout, ...]}
270    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
271        // TODO: unify layout code with Menu
272
273        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.width,
293            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                // items_node
383                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        // println!("bar_bounds: {:?}", bar_bounds);
431        // println!("cursor: {:?}", cursor);
432        // println!("cursor in bar_bounds: {:?}", cursor.is_over(bar_bounds));
433
434        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                // check if scrolling is on
495                {
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}