cosmic/widget/menu/
menu_bar.rs

1// From iced_aw, license MIT
2
3//! A widget that handles menu trees
4use super::{
5    menu_inner::{
6        CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight,
7    },
8    menu_tree::MenuTree,
9};
10use crate::style::menu_bar::StyleSheet;
11
12use iced::{Point, Vector};
13use iced_core::Border;
14use iced_widget::core::{
15    Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event,
16    layout::{Limits, Node},
17    mouse::{self, Cursor},
18    overlay, renderer, touch,
19    widget::{Tree, tree},
20};
21
22/// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing.
23pub fn menu_bar<Message, Renderer: iced_core::Renderer>(
24    menu_roots: Vec<MenuTree<Message, Renderer>>,
25) -> MenuBar<Message, Renderer> {
26    MenuBar::new(menu_roots)
27}
28
29pub(crate) struct MenuBarState {
30    pub(crate) pressed: bool,
31    pub(crate) view_cursor: Cursor,
32    pub(crate) open: bool,
33    pub(crate) active_root: Option<usize>,
34    pub(crate) horizontal_direction: Direction,
35    pub(crate) vertical_direction: Direction,
36    pub(crate) menu_states: Vec<MenuState>,
37}
38impl MenuBarState {
39    pub(super) fn get_trimmed_indices(&self) -> impl Iterator<Item = usize> + '_ {
40        self.menu_states
41            .iter()
42            .take_while(|ms| ms.index.is_some())
43            .map(|ms| ms.index.expect("No indices were found in the menu state."))
44    }
45
46    pub(super) fn reset(&mut self) {
47        self.open = false;
48        self.active_root = None;
49        self.menu_states.clear();
50    }
51}
52impl Default for MenuBarState {
53    fn default() -> Self {
54        Self {
55            pressed: false,
56            view_cursor: Cursor::Available([-0.5, -0.5].into()),
57            open: false,
58            active_root: None,
59            horizontal_direction: Direction::Positive,
60            vertical_direction: Direction::Positive,
61            menu_states: Vec::new(),
62        }
63    }
64}
65
66pub(crate) fn menu_roots_children<Message, Renderer>(
67    menu_roots: &Vec<MenuTree<'_, Message, Renderer>>,
68) -> Vec<Tree>
69where
70    Renderer: renderer::Renderer,
71{
72    /*
73    menu bar
74        menu root 1 (stateless)
75            flat tree
76        menu root 2 (stateless)
77            flat tree
78        ...
79    */
80
81    menu_roots
82        .iter()
83        .map(|root| {
84            let mut tree = Tree::empty();
85            let flat = root
86                .flattern()
87                .iter()
88                .map(|mt| Tree::new(mt.item.as_widget()))
89                .collect();
90            tree.children = flat;
91            tree
92        })
93        .collect()
94}
95
96#[allow(invalid_reference_casting)]
97pub(crate) fn menu_roots_diff<Message, Renderer>(
98    menu_roots: &mut Vec<MenuTree<'_, Message, Renderer>>,
99    tree: &mut Tree,
100) where
101    Renderer: renderer::Renderer,
102{
103    if tree.children.len() > menu_roots.len() {
104        tree.children.truncate(menu_roots.len());
105    }
106
107    tree.children
108        .iter_mut()
109        .zip(menu_roots.iter())
110        .for_each(|(t, root)| {
111            let mut flat = root
112                .flattern()
113                .iter()
114                .map(|mt| {
115                    let widget = mt.item.as_widget();
116                    let widget_ptr = widget as *const dyn Widget<Message, crate::Theme, Renderer>;
117                    let widget_ptr_mut =
118                        widget_ptr as *mut dyn Widget<Message, crate::Theme, Renderer>;
119                    //TODO: find a way to diff_children without unsafe code
120                    unsafe { &mut *widget_ptr_mut }
121                })
122                .collect::<Vec<_>>();
123
124            t.diff_children(flat.as_mut_slice());
125        });
126
127    if tree.children.len() < menu_roots.len() {
128        let extended = menu_roots[tree.children.len()..].iter().map(|root| {
129            let mut tree = Tree::empty();
130            let flat = root
131                .flattern()
132                .iter()
133                .map(|mt| Tree::new(mt.item.as_widget()))
134                .collect();
135            tree.children = flat;
136            tree
137        });
138        tree.children.extend(extended);
139    }
140}
141
142/// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing.
143#[allow(missing_debug_implementations)]
144pub struct MenuBar<'a, Message, Renderer = crate::Renderer>
145where
146    Renderer: renderer::Renderer,
147{
148    width: Length,
149    height: Length,
150    spacing: f32,
151    padding: Padding,
152    bounds_expand: u16,
153    main_offset: i32,
154    cross_offset: i32,
155    close_condition: CloseCondition,
156    item_width: ItemWidth,
157    item_height: ItemHeight,
158    path_highlight: Option<PathHighlight>,
159    menu_roots: Vec<MenuTree<'a, Message, Renderer>>,
160    style: <crate::Theme as StyleSheet>::Style,
161}
162
163impl<'a, Message, Renderer> MenuBar<'a, Message, Renderer>
164where
165    Renderer: renderer::Renderer,
166{
167    /// Creates a new [`MenuBar`] with the given menu roots
168    #[must_use]
169    pub fn new(menu_roots: Vec<MenuTree<'a, Message, Renderer>>) -> Self {
170        let mut menu_roots = menu_roots;
171        menu_roots.iter_mut().for_each(MenuTree::set_index);
172
173        Self {
174            width: Length::Shrink,
175            height: Length::Shrink,
176            spacing: 0.0,
177            padding: Padding::ZERO,
178            bounds_expand: 16,
179            main_offset: 0,
180            cross_offset: 0,
181            close_condition: CloseCondition {
182                leave: false,
183                click_outside: true,
184                click_inside: true,
185            },
186            item_width: ItemWidth::Uniform(150),
187            item_height: ItemHeight::Uniform(30),
188            path_highlight: Some(PathHighlight::MenuActive),
189            menu_roots,
190            style: <crate::Theme as StyleSheet>::Style::default(),
191        }
192    }
193
194    /// Sets the expand value for each menu's check bounds
195    ///
196    /// When the cursor goes outside of a menu's check bounds,
197    /// the menu will be closed automatically, this value expands
198    /// the check bounds
199    #[must_use]
200    pub fn bounds_expand(mut self, value: u16) -> Self {
201        self.bounds_expand = value;
202        self
203    }
204
205    /// [`CloseCondition`]
206    #[must_use]
207    pub fn close_condition(mut self, close_condition: CloseCondition) -> Self {
208        self.close_condition = close_condition;
209        self
210    }
211
212    /// Moves each menu in the horizontal open direction
213    #[must_use]
214    pub fn cross_offset(mut self, value: i32) -> Self {
215        self.cross_offset = value;
216        self
217    }
218
219    /// Sets the height of the [`MenuBar`]
220    #[must_use]
221    pub fn height(mut self, height: Length) -> Self {
222        self.height = height;
223        self
224    }
225
226    /// [`ItemHeight`]
227    #[must_use]
228    pub fn item_height(mut self, item_height: ItemHeight) -> Self {
229        self.item_height = item_height;
230        self
231    }
232
233    /// [`ItemWidth`]
234    #[must_use]
235    pub fn item_width(mut self, item_width: ItemWidth) -> Self {
236        self.item_width = item_width;
237        self
238    }
239
240    /// Moves all the menus in the vertical open direction
241    #[must_use]
242    pub fn main_offset(mut self, value: i32) -> Self {
243        self.main_offset = value;
244        self
245    }
246
247    /// Sets the [`Padding`] of the [`MenuBar`]
248    #[must_use]
249    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
250        self.padding = padding.into();
251        self
252    }
253
254    /// Sets the method for drawing path highlight
255    #[must_use]
256    pub fn path_highlight(mut self, path_highlight: Option<PathHighlight>) -> Self {
257        self.path_highlight = path_highlight;
258        self
259    }
260
261    /// Sets the spacing between menu roots
262    #[must_use]
263    pub fn spacing(mut self, units: f32) -> Self {
264        self.spacing = units;
265        self
266    }
267
268    /// Sets the style of the menu bar and its menus
269    #[must_use]
270    pub fn style(mut self, style: impl Into<<crate::Theme as StyleSheet>::Style>) -> Self {
271        self.style = style.into();
272        self
273    }
274
275    /// Sets the width of the [`MenuBar`]
276    #[must_use]
277    pub fn width(mut self, width: Length) -> Self {
278        self.width = width;
279        self
280    }
281}
282impl<Message, Renderer> Widget<Message, crate::Theme, Renderer> for MenuBar<'_, Message, Renderer>
283where
284    Renderer: renderer::Renderer,
285{
286    fn size(&self) -> iced_core::Size<Length> {
287        iced_core::Size::new(self.width, self.height)
288    }
289
290    fn diff(&mut self, tree: &mut Tree) {
291        menu_roots_diff(&mut self.menu_roots, tree);
292    }
293
294    fn tag(&self) -> tree::Tag {
295        tree::Tag::of::<MenuBarState>()
296    }
297
298    fn state(&self) -> tree::State {
299        tree::State::new(MenuBarState::default())
300    }
301
302    fn children(&self) -> Vec<Tree> {
303        menu_roots_children(&self.menu_roots)
304    }
305
306    fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
307        use super::flex;
308
309        let limits = limits.width(self.width).height(self.height);
310        let children = self
311            .menu_roots
312            .iter()
313            .map(|root| &root.item)
314            .collect::<Vec<_>>();
315        // the first children of the tree are the menu roots items
316        let mut tree_children = tree
317            .children
318            .iter_mut()
319            .map(|t| &mut t.children[0])
320            .collect::<Vec<_>>();
321        flex::resolve(
322            &flex::Axis::Horizontal,
323            renderer,
324            &limits,
325            self.padding,
326            self.spacing,
327            Alignment::Center,
328            &children,
329            &mut tree_children,
330        )
331    }
332
333    fn on_event(
334        &mut self,
335        tree: &mut Tree,
336        event: event::Event,
337        layout: Layout<'_>,
338        view_cursor: Cursor,
339        renderer: &Renderer,
340        clipboard: &mut dyn Clipboard,
341        shell: &mut Shell<'_, Message>,
342        viewport: &Rectangle,
343    ) -> event::Status {
344        use event::Event::{Mouse, Touch};
345        use mouse::{Button::Left, Event::ButtonReleased};
346        use touch::Event::{FingerLifted, FingerLost};
347
348        let root_status = process_root_events(
349            &mut self.menu_roots,
350            view_cursor,
351            tree,
352            &event,
353            layout,
354            renderer,
355            clipboard,
356            shell,
357            viewport,
358        );
359
360        let state = tree.state.downcast_mut::<MenuBarState>();
361
362        match event {
363            Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => {
364                if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
365                    state.view_cursor = view_cursor;
366                    state.open = true;
367                    // #[cfg(feature = "wayland")]
368                    // TODO emit Message to open menu
369                }
370            }
371            _ => (),
372        }
373        root_status
374    }
375
376    fn draw(
377        &self,
378        tree: &Tree,
379        renderer: &mut Renderer,
380        theme: &crate::Theme,
381        style: &renderer::Style,
382        layout: Layout<'_>,
383        view_cursor: Cursor,
384        viewport: &Rectangle,
385    ) {
386        let state = tree.state.downcast_ref::<MenuBarState>();
387        let cursor_pos = view_cursor.position().unwrap_or_default();
388        let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) {
389            state.view_cursor
390        } else {
391            view_cursor
392        };
393
394        // draw path highlight
395        if self.path_highlight.is_some() {
396            let styling = theme.appearance(&self.style);
397            if let Some(active) = state.active_root {
398                let active_bounds = layout
399                    .children()
400                    .nth(active)
401                    .expect("Active child not found in menu?")
402                    .bounds();
403                let path_quad = renderer::Quad {
404                    bounds: active_bounds,
405                    border: Border {
406                        radius: styling.bar_border_radius.into(),
407                        ..Default::default()
408                    },
409                    shadow: Default::default(),
410                };
411
412                renderer.fill_quad(path_quad, styling.path);
413            }
414        }
415
416        self.menu_roots
417            .iter()
418            .zip(&tree.children)
419            .zip(layout.children())
420            .for_each(|((root, t), lo)| {
421                root.item.as_widget().draw(
422                    &t.children[root.index],
423                    renderer,
424                    theme,
425                    style,
426                    lo,
427                    position,
428                    viewport,
429                );
430            });
431    }
432
433    fn overlay<'b>(
434        &'b mut self,
435        tree: &'b mut Tree,
436        layout: Layout<'_>,
437        _renderer: &Renderer,
438        translation: Vector,
439    ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
440        // #[cfg(feature = "wayland")]
441        // return None;
442
443        let state = tree.state.downcast_ref::<MenuBarState>();
444        if !state.open {
445            return None;
446        }
447
448        Some(
449            Menu {
450                tree,
451                menu_roots: &mut self.menu_roots,
452                bounds_expand: self.bounds_expand,
453                menu_overlays_parent: false,
454                close_condition: self.close_condition,
455                item_width: self.item_width,
456                item_height: self.item_height,
457                bar_bounds: layout.bounds(),
458                main_offset: self.main_offset,
459                cross_offset: self.cross_offset,
460                root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(),
461                path_highlight: self.path_highlight,
462                style: &self.style,
463                position: Point::new(translation.x, translation.y),
464            }
465            .overlay(),
466        )
467    }
468}
469impl<'a, Message, Renderer> From<MenuBar<'a, Message, Renderer>>
470    for Element<'a, Message, crate::Theme, Renderer>
471where
472    Message: 'a,
473    Renderer: 'a + renderer::Renderer,
474{
475    fn from(value: MenuBar<'a, Message, Renderer>) -> Self {
476        Self::new(value)
477    }
478}
479
480#[allow(unused_results, clippy::too_many_arguments)]
481fn process_root_events<Message, Renderer>(
482    menu_roots: &mut [MenuTree<'_, Message, Renderer>],
483    view_cursor: Cursor,
484    tree: &mut Tree,
485    event: &event::Event,
486    layout: Layout<'_>,
487    renderer: &Renderer,
488    clipboard: &mut dyn Clipboard,
489    shell: &mut Shell<'_, Message>,
490    viewport: &Rectangle,
491) -> event::Status
492where
493    Renderer: renderer::Renderer,
494{
495    menu_roots
496        .iter_mut()
497        .zip(&mut tree.children)
498        .zip(layout.children())
499        .map(|((root, t), lo)| {
500            // assert!(t.tag == tree::Tag::stateless());
501            root.item.as_widget_mut().on_event(
502                &mut t.children[root.index],
503                event.clone(),
504                lo,
505                view_cursor,
506                renderer,
507                clipboard,
508                shell,
509                viewport,
510            )
511        })
512        .fold(event::Status::Ignored, event::Status::merge)
513}