cosmic/widget/menu/
menu_inner.rs

1// From iced_aw, license MIT
2
3//! Menu tree overlay
4use super::{menu_bar::MenuBarState, menu_tree::MenuTree};
5use crate::style::menu_bar::StyleSheet;
6
7use iced_core::{Border, Shadow};
8use iced_widget::core::{
9    Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event,
10    layout::{Limits, Node},
11    mouse::{self, Cursor},
12    overlay, renderer, touch,
13    widget::Tree,
14};
15
16/// The condition of when to close a menu
17#[derive(Debug, Clone, Copy)]
18pub struct CloseCondition {
19    /// Close menus when the cursor moves outside the check bounds
20    pub leave: bool,
21
22    /// Close menus when the cursor clicks outside the check bounds
23    pub click_outside: bool,
24
25    /// Close menus when the cursor clicks inside the check bounds
26    pub click_inside: bool,
27}
28
29/// The width of an item
30#[derive(Debug, Clone, Copy)]
31pub enum ItemWidth {
32    /// Use uniform width
33    Uniform(u16),
34    /// Static tries to use the width value of each menu(menu tree with children),
35    /// the widths of items(menu tree with empty children) will be the same as the menu they're in,
36    /// if that value is None,
37    /// the default value will be used instead,
38    /// which is the value of the Static variant
39    Static(u16),
40}
41
42/// The height of an item
43#[derive(Debug, Clone, Copy)]
44pub enum ItemHeight {
45    /// Use uniform height.
46    Uniform(u16),
47    /// Static tries to use `MenuTree.height` as item height,
48    /// when it's `None` it'll fallback to the value of the `Static` variant.
49    Static(u16),
50    /// Dynamic tries to automatically choose the proper item height for you,
51    /// but it only works in certain cases:
52    ///
53    /// - Fixed height
54    /// - Shrink height
55    /// - Menu tree height
56    ///
57    /// If none of these is the case, it'll fallback to the value of the `Dynamic` variant.
58    Dynamic(u16),
59}
60
61/// Methods for drawing path highlight
62#[derive(Debug, Clone, Copy)]
63pub enum PathHighlight {
64    /// Draw the full path,
65    Full,
66    /// Omit the active item(the last item in the path)
67    OmitActive,
68    /// Omit the active item if it's not a menu
69    MenuActive,
70}
71
72/// X+ goes right and Y+ goes down
73#[derive(Debug, Clone, Copy)]
74pub(crate) enum Direction {
75    Positive,
76    Negative,
77}
78
79/// Adaptive open direction
80#[derive(Debug)]
81#[allow(clippy::struct_excessive_bools)]
82struct Aod {
83    // whether or not to use aod
84    horizontal: bool,
85    vertical: bool,
86
87    // whether or not to use overlap
88    horizontal_overlap: bool,
89    vertical_overlap: bool,
90
91    // default direction
92    horizontal_direction: Direction,
93    vertical_direction: Direction,
94
95    // Offset of the child in the default direction
96    horizontal_offset: f32,
97    vertical_offset: f32,
98}
99impl Aod {
100    /// Returns child position and offset position
101    #[allow(clippy::too_many_arguments)]
102    fn adaptive(
103        parent_pos: f32,
104        parent_size: f32,
105        child_size: f32,
106        max_size: f32,
107        offset: f32,
108        on: bool,
109        overlap: bool,
110        direction: Direction,
111    ) -> (f32, f32) {
112        /*
113        Imagine there're two sticks, parent and child
114        parent: o-----o
115        child:  o----------o
116
117        Now we align the child to the parent in one dimension
118        There are 4 possibilities:
119
120        1. to the right
121                    o-----oo----------o
122
123        2. to the right but allow overlaping
124                    o-----o
125                    o----------o
126
127        3. to the left
128        o----------oo-----o
129
130        4. to the left but allow overlaping
131                    o-----o
132               o----------o
133
134        The child goes to the default direction by default,
135        if the space on the default direction runs out it goes to the the other,
136        whether to use overlap is the caller's decision
137
138        This can be applied to any direction
139        */
140
141        match direction {
142            Direction::Positive => {
143                let space_negative = parent_pos;
144                let space_positive = max_size - parent_pos - parent_size;
145
146                if overlap {
147                    let overshoot = child_size - parent_size;
148                    if on && space_negative > space_positive && overshoot > space_positive {
149                        (parent_pos - overshoot, parent_pos - overshoot)
150                    } else {
151                        (parent_pos, parent_pos)
152                    }
153                } else {
154                    let overshoot = child_size + offset;
155                    if on && space_negative > space_positive && overshoot > space_positive {
156                        (parent_pos - overshoot, parent_pos - offset)
157                    } else {
158                        (parent_pos + parent_size + offset, parent_pos + parent_size)
159                    }
160                }
161            }
162            Direction::Negative => {
163                let space_positive = parent_pos;
164                let space_negative = max_size - parent_pos - parent_size;
165
166                if overlap {
167                    let overshoot = child_size - parent_size;
168                    if on && space_negative > space_positive && overshoot > space_positive {
169                        (parent_pos, parent_pos)
170                    } else {
171                        (parent_pos - overshoot, parent_pos - overshoot)
172                    }
173                } else {
174                    let overshoot = child_size + offset;
175                    if on && space_negative > space_positive && overshoot > space_positive {
176                        (parent_pos + parent_size + offset, parent_pos + parent_size)
177                    } else {
178                        (parent_pos - overshoot, parent_pos - offset)
179                    }
180                }
181            }
182        }
183    }
184
185    /// Returns child position and offset position
186    fn resolve(
187        &self,
188        parent_bounds: Rectangle,
189        children_size: Size,
190        viewport_size: Size,
191    ) -> (Point, Point) {
192        let (x, ox) = Self::adaptive(
193            parent_bounds.x,
194            parent_bounds.width,
195            children_size.width,
196            viewport_size.width,
197            self.horizontal_offset,
198            self.horizontal,
199            self.horizontal_overlap,
200            self.horizontal_direction,
201        );
202        let (y, oy) = Self::adaptive(
203            parent_bounds.y,
204            parent_bounds.height,
205            children_size.height,
206            viewport_size.height,
207            self.vertical_offset,
208            self.vertical,
209            self.vertical_overlap,
210            self.vertical_direction,
211        );
212
213        ([x, y].into(), [ox, oy].into())
214    }
215}
216
217/// A part of a menu where items are displayed.
218///
219/// When the bounds of a menu exceed the viewport,
220/// only items inside the viewport will be displayed,
221/// when scrolling happens, this should be updated
222#[derive(Debug, Clone, Copy)]
223pub(super) struct MenuSlice {
224    pub(super) start_index: usize,
225    pub(super) end_index: usize,
226    pub(super) lower_bound_rel: f32,
227    pub(super) upper_bound_rel: f32,
228}
229
230/// Menu bounds in overlay space
231struct MenuBounds {
232    child_positions: Vec<f32>,
233    child_sizes: Vec<Size>,
234    children_bounds: Rectangle,
235    parent_bounds: Rectangle,
236    check_bounds: Rectangle,
237    offset_bounds: Rectangle,
238}
239impl MenuBounds {
240    #[allow(clippy::too_many_arguments)]
241    fn new<Message, Renderer>(
242        menu_tree: &MenuTree<'_, Message, Renderer>,
243        renderer: &Renderer,
244        item_width: ItemWidth,
245        item_height: ItemHeight,
246        viewport_size: Size,
247        overlay_offset: Vector,
248        aod: &Aod,
249        bounds_expand: u16,
250        parent_bounds: Rectangle,
251        tree: &mut [Tree],
252    ) -> Self
253    where
254        Renderer: renderer::Renderer,
255    {
256        let (children_size, child_positions, child_sizes) =
257            get_children_layout(menu_tree, renderer, item_width, item_height, tree);
258
259        // viewport space parent bounds
260        let view_parent_bounds = parent_bounds + overlay_offset;
261
262        // overlay space children position
263        let (children_position, offset_position) = {
264            let (cp, op) = aod.resolve(view_parent_bounds, children_size, viewport_size);
265            (cp - overlay_offset, op - overlay_offset)
266        };
267
268        // calc offset bounds
269        let delta = children_position - offset_position;
270        let offset_size = if delta.x.abs() > delta.y.abs() {
271            Size::new(delta.x, children_size.height)
272        } else {
273            Size::new(children_size.width, delta.y)
274        };
275        let offset_bounds = Rectangle::new(offset_position, offset_size);
276
277        let children_bounds = Rectangle::new(children_position, children_size);
278        let check_bounds = pad_rectangle(children_bounds, bounds_expand.into());
279
280        Self {
281            child_positions,
282            child_sizes,
283            children_bounds,
284            parent_bounds,
285            check_bounds,
286            offset_bounds,
287        }
288    }
289}
290
291pub(crate) struct MenuState {
292    pub(super) index: Option<usize>,
293    scroll_offset: f32,
294    menu_bounds: MenuBounds,
295}
296impl MenuState {
297    pub(super) fn layout<Message, Renderer>(
298        &self,
299        overlay_offset: Vector,
300        slice: MenuSlice,
301        renderer: &Renderer,
302        menu_tree: &MenuTree<'_, Message, Renderer>,
303        tree: &mut [Tree],
304    ) -> Node
305    where
306        Renderer: renderer::Renderer,
307    {
308        let MenuSlice {
309            start_index,
310            end_index,
311            lower_bound_rel,
312            upper_bound_rel,
313        } = slice;
314
315        assert_eq!(
316            menu_tree.children.len(),
317            self.menu_bounds.child_positions.len()
318        );
319
320        // viewport space children bounds
321        let children_bounds = self.menu_bounds.children_bounds + overlay_offset;
322
323        let child_nodes = self.menu_bounds.child_positions[start_index..=end_index]
324            .iter()
325            .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter())
326            .zip(menu_tree.children[start_index..=end_index].iter())
327            .map(|((cp, size), mt)| {
328                let mut position = *cp;
329                let mut size = *size;
330
331                if position < lower_bound_rel && (position + size.height) > lower_bound_rel {
332                    size.height = position + size.height - lower_bound_rel;
333                    position = lower_bound_rel;
334                } else if position <= upper_bound_rel && (position + size.height) > upper_bound_rel
335                {
336                    size.height = upper_bound_rel - position;
337                }
338
339                let limits = Limits::new(Size::ZERO, size);
340
341                mt.item
342                    .as_widget()
343                    .layout(&mut tree[mt.index], renderer, &limits)
344                    .move_to(Point::new(0.0, position + self.scroll_offset))
345            })
346            .collect::<Vec<_>>();
347
348        Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position())
349    }
350
351    fn layout_single<Message, Renderer>(
352        &self,
353        overlay_offset: Vector,
354        index: usize,
355        renderer: &Renderer,
356        menu_tree: &MenuTree<'_, Message, Renderer>,
357        tree: &mut Tree,
358    ) -> Node
359    where
360        Renderer: renderer::Renderer,
361    {
362        // viewport space children bounds
363        let children_bounds = self.menu_bounds.children_bounds + overlay_offset;
364
365        let position = self.menu_bounds.child_positions[index];
366        let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]);
367        let parent_offset = children_bounds.position() - Point::ORIGIN;
368        let node = menu_tree.item.as_widget().layout(tree, renderer, &limits);
369        node.clone().move_to(Point::new(
370            parent_offset.x,
371            parent_offset.y + position + self.scroll_offset,
372        ))
373    }
374
375    pub(super) fn slice(
376        &self,
377        viewport_size: Size,
378        overlay_offset: Vector,
379        item_height: ItemHeight,
380    ) -> MenuSlice {
381        // viewport space children bounds
382        let children_bounds = self.menu_bounds.children_bounds + overlay_offset;
383
384        let max_index = self.menu_bounds.child_positions.len().saturating_sub(1);
385
386        // viewport space absolute bounds
387        let lower_bound = children_bounds.y.max(0.0);
388        let upper_bound = (children_bounds.y + children_bounds.height).min(viewport_size.height);
389
390        // menu space relative bounds
391        let lower_bound_rel = lower_bound - (children_bounds.y + self.scroll_offset);
392        let upper_bound_rel = upper_bound - (children_bounds.y + self.scroll_offset);
393
394        // index range
395        let (start_index, end_index) = match item_height {
396            ItemHeight::Uniform(u) => {
397                let start_index = (lower_bound_rel / f32::from(u)).floor() as usize;
398                let end_index = ((upper_bound_rel / f32::from(u)).floor() as usize).min(max_index);
399                (start_index, end_index)
400            }
401            ItemHeight::Static(_) | ItemHeight::Dynamic(_) => {
402                let positions = &self.menu_bounds.child_positions;
403                let sizes = &self.menu_bounds.child_sizes;
404
405                let start_index = search_bound(0, 0, max_index, lower_bound_rel, positions, sizes);
406                let end_index = search_bound(
407                    max_index,
408                    start_index,
409                    max_index,
410                    upper_bound_rel,
411                    positions,
412                    sizes,
413                )
414                .min(max_index);
415
416                (start_index, end_index)
417            }
418        };
419
420        MenuSlice {
421            start_index,
422            end_index,
423            lower_bound_rel,
424            upper_bound_rel,
425        }
426    }
427}
428
429pub(crate) struct Menu<'a, 'b, Message, Renderer>
430where
431    Renderer: renderer::Renderer,
432{
433    pub(crate) tree: &'b mut Tree,
434    pub(crate) menu_roots: &'b mut Vec<MenuTree<'a, Message, Renderer>>,
435    pub(crate) bounds_expand: u16,
436    /// Allows menu overlay items to overlap the parent
437    pub(crate) menu_overlays_parent: bool,
438    pub(crate) close_condition: CloseCondition,
439    pub(crate) item_width: ItemWidth,
440    pub(crate) item_height: ItemHeight,
441    pub(crate) bar_bounds: Rectangle,
442    pub(crate) main_offset: i32,
443    pub(crate) cross_offset: i32,
444    pub(crate) root_bounds_list: Vec<Rectangle>,
445    pub(crate) path_highlight: Option<PathHighlight>,
446    pub(crate) style: &'b <crate::Theme as StyleSheet>::Style,
447    pub(crate) position: Point,
448}
449impl<'b, Message, Renderer> Menu<'_, 'b, Message, Renderer>
450where
451    Renderer: renderer::Renderer,
452{
453    pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, Renderer> {
454        overlay::Element::new(Box::new(self))
455    }
456}
457impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
458    for Menu<'_, '_, Message, Renderer>
459where
460    Renderer: renderer::Renderer,
461{
462    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node {
463        // layout children
464        let position = self.position;
465        let state = self.tree.state.downcast_mut::<MenuBarState>();
466        let overlay_offset = Point::ORIGIN - position;
467        let tree_children = &mut self.tree.children;
468        let children = state
469            .active_root
470            .map(|active_root| {
471                let root = &self.menu_roots[active_root];
472                let active_tree = &mut tree_children[active_root];
473                state.menu_states.iter().enumerate().fold(
474                    (root, Vec::new()),
475                    |(menu_root, mut nodes), (_i, ms)| {
476                        let slice = ms.slice(bounds, overlay_offset, self.item_height);
477                        let _start_index = slice.start_index;
478                        let _end_index = slice.end_index;
479                        let children_node = ms.layout(
480                            overlay_offset,
481                            slice,
482                            renderer,
483                            menu_root,
484                            &mut active_tree.children,
485                        );
486                        nodes.push(children_node);
487                        // only the last menu can have a None active index
488                        (
489                            ms.index
490                                .map_or(menu_root, |active| &menu_root.children[active]),
491                            nodes,
492                        )
493                    },
494                )
495            })
496            .map(|(_, l)| l)
497            .unwrap_or_default();
498
499        // overlay space viewport rectangle
500        Node::with_children(bounds, children).translate(Point::ORIGIN - position)
501    }
502
503    fn on_event(
504        &mut self,
505        event: event::Event,
506        layout: Layout<'_>,
507        view_cursor: Cursor,
508        renderer: &Renderer,
509        clipboard: &mut dyn Clipboard,
510        shell: &mut Shell<'_, Message>,
511    ) -> event::Status {
512        use event::{
513            Event::{Mouse, Touch},
514            Status::{Captured, Ignored},
515        };
516        use mouse::{
517            Button::Left,
518            Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled},
519        };
520        use touch::Event::{FingerLifted, FingerMoved, FingerPressed};
521
522        if !self.tree.state.downcast_ref::<MenuBarState>().open {
523            return Ignored;
524        };
525
526        let viewport = layout.bounds();
527        let viewport_size = viewport.size();
528        let overlay_offset = Point::ORIGIN - viewport.position();
529        let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
530
531        let menu_status = process_menu_events(
532            self.tree,
533            self.menu_roots,
534            event.clone(),
535            view_cursor,
536            renderer,
537            clipboard,
538            shell,
539            overlay_offset,
540        );
541
542        init_root_menu(
543            self,
544            renderer,
545            shell,
546            overlay_cursor,
547            viewport_size,
548            overlay_offset,
549            self.bar_bounds,
550            self.main_offset as f32,
551        );
552
553        match event {
554            Mouse(WheelScrolled { delta }) => {
555                process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset)
556                    .merge(menu_status)
557            }
558
559            Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => {
560                let state = self.tree.state.downcast_mut::<MenuBarState>();
561                state.pressed = true;
562                state.view_cursor = view_cursor;
563                Captured
564            }
565
566            Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => {
567                let view_cursor = Cursor::Available(position);
568                let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
569                process_overlay_events(
570                    self,
571                    renderer,
572                    viewport_size,
573                    overlay_offset,
574                    view_cursor,
575                    overlay_cursor,
576                    self.cross_offset as f32,
577                )
578                .merge(menu_status)
579            }
580
581            Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => {
582                let state = self.tree.state.downcast_mut::<MenuBarState>();
583                state.pressed = false;
584
585                // process close condition
586                if state
587                    .view_cursor
588                    .position()
589                    .unwrap_or_default()
590                    .distance(view_cursor.position().unwrap_or_default())
591                    < 2.0
592                {
593                    let is_inside = state
594                        .menu_states
595                        .iter()
596                        .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor));
597
598                    if self.close_condition.click_inside
599                        && is_inside
600                        && matches!(
601                            event,
602                            Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. })
603                        )
604                    {
605                        state.reset();
606                        return Captured;
607                    }
608
609                    if self.close_condition.click_outside && !is_inside {
610                        state.reset();
611                        return Captured;
612                    }
613                }
614
615                // close all menus when clicking inside the menu bar
616                if self.bar_bounds.contains(overlay_cursor) {
617                    state.reset();
618                    Captured
619                } else {
620                    menu_status
621                }
622            }
623
624            _ => menu_status,
625        }
626    }
627
628    #[allow(unused_results)]
629    fn draw(
630        &self,
631        renderer: &mut Renderer,
632        theme: &crate::Theme,
633        style: &renderer::Style,
634        layout: Layout<'_>,
635        view_cursor: Cursor,
636    ) {
637        let state = self.tree.state.downcast_ref::<MenuBarState>();
638        let Some(active_root) = state.active_root else {
639            return;
640        };
641
642        let viewport = layout.bounds();
643        let viewport_size = viewport.size();
644        let overlay_offset = Point::ORIGIN - viewport.position();
645        let render_bounds = Rectangle::new(Point::ORIGIN, viewport.size());
646
647        let styling = theme.appearance(self.style);
648
649        let tree = &self.tree.children[active_root].children;
650        let root = &self.menu_roots[active_root];
651
652        let indices = state.get_trimmed_indices().collect::<Vec<_>>();
653
654        state
655            .menu_states
656            .iter()
657            .zip(layout.children())
658            .enumerate()
659            .fold(root, |menu_root, (i, (ms, children_layout))| {
660                let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph {
661                    PathHighlight::Full => true,
662                    PathHighlight::OmitActive => !indices.is_empty() && i < indices.len() - 1,
663                    PathHighlight::MenuActive => i < state.menu_states.len() - 1,
664                });
665
666                // react only to the last menu
667                let view_cursor = if i == state.menu_states.len() - 1 {
668                    view_cursor
669                } else {
670                    Cursor::Available([-1.0; 2].into())
671                };
672
673                let draw_menu = |r: &mut Renderer| {
674                    // calc slice
675                    let slice = ms.slice(viewport_size, overlay_offset, self.item_height);
676                    let start_index = slice.start_index;
677                    let end_index = slice.end_index;
678
679                    let children_bounds = children_layout.bounds();
680
681                    // draw menu background
682                    // let bounds = pad_rectangle(children_bounds, styling.background_expand.into());
683                    // println!("cursor: {:?}", view_cursor);
684                    // println!("bg_bounds: {:?}", bounds);
685                    // println!("color: {:?}\n", styling.background);
686                    let menu_quad = renderer::Quad {
687                        bounds: pad_rectangle(children_bounds, styling.background_expand.into()),
688                        border: Border {
689                            radius: styling.menu_border_radius.into(),
690                            width: styling.border_width,
691                            color: styling.border_color,
692                        },
693                        shadow: Shadow::default(),
694                    };
695                    let menu_color = styling.background;
696                    r.fill_quad(menu_quad, menu_color);
697
698                    // draw path hightlight
699                    if let (true, Some(active)) = (draw_path, ms.index) {
700                        let active_bounds = children_layout
701                            .children()
702                            .nth(active.saturating_sub(start_index))
703                            .expect("No active children were found in menu?")
704                            .bounds();
705                        let path_quad = renderer::Quad {
706                            bounds: active_bounds,
707                            border: Border {
708                                radius: styling.menu_border_radius.into(),
709                                ..Default::default()
710                            },
711                            shadow: Shadow::default(),
712                        };
713
714                        r.fill_quad(path_quad, styling.path);
715                    }
716
717                    // draw item
718                    menu_root.children[start_index..=end_index]
719                        .iter()
720                        .zip(children_layout.children())
721                        .for_each(|(mt, clo)| {
722                            mt.item.as_widget().draw(
723                                &tree[mt.index],
724                                r,
725                                theme,
726                                style,
727                                clo,
728                                view_cursor,
729                                &children_layout.bounds(),
730                            );
731                        });
732                };
733
734                renderer.with_layer(render_bounds, draw_menu);
735
736                // only the last menu can have a None active index
737                ms.index
738                    .map_or(menu_root, |active| &menu_root.children[active])
739            });
740    }
741}
742
743fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle {
744    Rectangle {
745        x: rect.x - padding.left,
746        y: rect.y - padding.top,
747        width: rect.width + padding.horizontal(),
748        height: rect.height + padding.vertical(),
749    }
750}
751
752pub(super) fn init_root_menu<Message, Renderer>(
753    menu: &mut Menu<'_, '_, Message, Renderer>,
754    renderer: &Renderer,
755    shell: &mut Shell<'_, Message>,
756    overlay_cursor: Point,
757    viewport_size: Size,
758    overlay_offset: Vector,
759    bar_bounds: Rectangle,
760    main_offset: f32,
761) where
762    Renderer: renderer::Renderer,
763{
764    let state = menu.tree.state.downcast_mut::<MenuBarState>();
765    if !(state.menu_states.is_empty() && bar_bounds.contains(overlay_cursor)) {
766        return;
767    }
768
769    for (i, (&root_bounds, mt)) in menu
770        .root_bounds_list
771        .iter()
772        .zip(menu.menu_roots.iter())
773        .enumerate()
774    {
775        if mt.children.is_empty() {
776            continue;
777        }
778
779        if root_bounds.contains(overlay_cursor) {
780            let view_center = viewport_size.width * 0.5;
781            let rb_center = root_bounds.center_x();
782
783            state.horizontal_direction = if rb_center > view_center {
784                Direction::Negative
785            } else {
786                Direction::Positive
787            };
788
789            let aod = Aod {
790                horizontal: true,
791                vertical: true,
792                horizontal_overlap: true,
793                vertical_overlap: false,
794                horizontal_direction: state.horizontal_direction,
795                vertical_direction: state.vertical_direction,
796                horizontal_offset: 0.0,
797                vertical_offset: main_offset,
798            };
799
800            let menu_bounds = MenuBounds::new(
801                mt,
802                renderer,
803                menu.item_width,
804                menu.item_height,
805                viewport_size,
806                overlay_offset,
807                &aod,
808                menu.bounds_expand,
809                root_bounds,
810                &mut menu.tree.children[i].children,
811            );
812
813            state.active_root = Some(i);
814            state.menu_states.push(MenuState {
815                index: None,
816                scroll_offset: 0.0,
817                menu_bounds,
818            });
819
820            // Hack to ensure menu opens properly
821            shell.invalidate_layout();
822
823            break;
824        }
825    }
826}
827
828#[allow(clippy::too_many_arguments)]
829fn process_menu_events<'b, Message, Renderer>(
830    tree: &'b mut Tree,
831    menu_roots: &'b mut [MenuTree<'_, Message, Renderer>],
832    event: event::Event,
833    view_cursor: Cursor,
834    renderer: &Renderer,
835    clipboard: &mut dyn Clipboard,
836    shell: &mut Shell<'_, Message>,
837    overlay_offset: Vector,
838) -> event::Status
839where
840    Renderer: renderer::Renderer,
841{
842    use event::Status;
843
844    let state = tree.state.downcast_mut::<MenuBarState>();
845    let Some(active_root) = state.active_root else {
846        return Status::Ignored;
847    };
848
849    let indices = state.get_trimmed_indices().collect::<Vec<_>>();
850
851    if indices.is_empty() {
852        return Status::Ignored;
853    }
854
855    // get active item
856    let mt = indices
857        .iter()
858        .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]);
859
860    // widget tree
861    let tree = &mut tree.children[active_root].children[mt.index];
862
863    // get layout
864    let last_ms = &state.menu_states[indices.len() - 1];
865    let child_node = last_ms.layout_single(
866        overlay_offset,
867        last_ms.index.expect("missing index within menu state."),
868        renderer,
869        mt,
870        tree,
871    );
872    let child_layout = Layout::new(&child_node);
873
874    // process only the last widget
875    mt.item.as_widget_mut().on_event(
876        tree,
877        event,
878        child_layout,
879        view_cursor,
880        renderer,
881        clipboard,
882        shell,
883        &Rectangle::default(),
884    )
885}
886
887#[allow(unused_results)]
888fn process_overlay_events<Message, Renderer>(
889    menu: &mut Menu<'_, '_, Message, Renderer>,
890    renderer: &Renderer,
891    viewport_size: Size,
892    overlay_offset: Vector,
893    view_cursor: Cursor,
894    overlay_cursor: Point,
895    cross_offset: f32,
896) -> event::Status
897where
898    Renderer: renderer::Renderer,
899{
900    use event::Status::{Captured, Ignored};
901    /*
902    if no active root || pressed:
903        return
904    else:
905        remove invalid menus // overlay space
906        update active item
907        if active item is a menu:
908            add menu // viewport space
909    */
910
911    let state = menu.tree.state.downcast_mut::<MenuBarState>();
912
913    let Some(active_root) = state.active_root else {
914        if !menu.bar_bounds.contains(overlay_cursor) {
915            state.reset();
916        }
917        return Ignored;
918    };
919
920    if state.pressed {
921        return Ignored;
922    }
923
924    /* When overlay is running, cursor_position in any widget method will go negative
925    but I still want Widget::draw() to react to cursor movement */
926    state.view_cursor = view_cursor;
927
928    // * remove invalid menus
929    let mut prev_bounds = std::iter::once(menu.bar_bounds)
930        .chain(
931            state.menu_states[..state.menu_states.len().saturating_sub(1)]
932                .iter()
933                .map(|ms| ms.menu_bounds.children_bounds),
934        )
935        .collect::<Vec<_>>();
936
937    if menu.close_condition.leave {
938        for i in (0..state.menu_states.len()).rev() {
939            let mb = &state.menu_states[i].menu_bounds;
940
941            if mb.parent_bounds.contains(overlay_cursor)
942                || mb.children_bounds.contains(overlay_cursor)
943                || mb.offset_bounds.contains(overlay_cursor)
944                || (mb.check_bounds.contains(overlay_cursor)
945                    && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)))
946            {
947                break;
948            }
949            prev_bounds.pop();
950            state.menu_states.pop();
951        }
952    } else {
953        for i in (0..state.menu_states.len()).rev() {
954            let mb = &state.menu_states[i].menu_bounds;
955
956            if mb.parent_bounds.contains(overlay_cursor)
957                || mb.children_bounds.contains(overlay_cursor)
958                || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))
959            {
960                break;
961            }
962            prev_bounds.pop();
963            state.menu_states.pop();
964        }
965    }
966
967    // get indices
968    let indices = state
969        .menu_states
970        .iter()
971        .map(|ms| ms.index)
972        .collect::<Vec<_>>();
973
974    // * update active item
975    let Some(last_menu_state) = state.menu_states.last_mut() else {
976        // no menus left
977        state.active_root = None;
978
979        // keep state.open when the cursor is still inside the menu bar
980        // this allows the overlay to keep drawing when the cursor is
981        // moving aroung the menu bar
982        if !menu.bar_bounds.contains(overlay_cursor) {
983            state.open = false;
984        }
985        return Captured;
986    };
987
988    let last_menu_bounds = &last_menu_state.menu_bounds;
989    let last_parent_bounds = last_menu_bounds.parent_bounds;
990    let last_children_bounds = last_menu_bounds.children_bounds;
991
992    if (!menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor))
993    // cursor is in the parent part
994    || !last_children_bounds.contains(overlay_cursor)
995    // cursor is outside
996    {
997        last_menu_state.index = None;
998        return Captured;
999    }
1000    // cursor is in the children part
1001
1002    // calc new index
1003    let height_diff = (overlay_cursor.y - (last_children_bounds.y + last_menu_state.scroll_offset))
1004        .clamp(0.0, last_children_bounds.height - 0.001);
1005
1006    let active_menu_root = &menu.menu_roots[active_root];
1007
1008    let active_menu = indices[0..indices.len().saturating_sub(1)]
1009        .iter()
1010        .fold(active_menu_root, |mt, i| {
1011            &mt.children[i.expect("missing active child index in menu")]
1012        });
1013
1014    let new_index = match menu.item_height {
1015        ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize,
1016        ItemHeight::Static(_) | ItemHeight::Dynamic(_) => {
1017            let max_index = active_menu.children.len() - 1;
1018            search_bound(
1019                0,
1020                0,
1021                max_index,
1022                height_diff,
1023                &last_menu_bounds.child_positions,
1024                &last_menu_bounds.child_sizes,
1025            )
1026        }
1027    };
1028
1029    // set new index
1030    last_menu_state.index = Some(new_index);
1031
1032    // get new active item
1033    let item = &active_menu.children[new_index];
1034
1035    // * add new menu if the new item is a menu
1036    if !item.children.is_empty() {
1037        let item_position = Point::new(
1038            0.0,
1039            last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset,
1040        );
1041        let item_size = last_menu_bounds.child_sizes[new_index];
1042
1043        // overlay space item bounds
1044        let item_bounds = Rectangle::new(item_position, item_size)
1045            + (last_menu_bounds.children_bounds.position() - Point::ORIGIN);
1046
1047        let aod = Aod {
1048            horizontal: true,
1049            vertical: true,
1050            horizontal_overlap: false,
1051            vertical_overlap: true,
1052            horizontal_direction: state.horizontal_direction,
1053            vertical_direction: state.vertical_direction,
1054            horizontal_offset: cross_offset,
1055            vertical_offset: 0.0,
1056        };
1057
1058        state.menu_states.push(MenuState {
1059            index: None,
1060            scroll_offset: 0.0,
1061            menu_bounds: MenuBounds::new(
1062                item,
1063                renderer,
1064                menu.item_width,
1065                menu.item_height,
1066                viewport_size,
1067                overlay_offset,
1068                &aod,
1069                menu.bounds_expand,
1070                item_bounds,
1071                &mut menu.tree.children[active_root].children,
1072            ),
1073        });
1074    }
1075
1076    Captured
1077}
1078
1079fn process_scroll_events<Message, Renderer>(
1080    menu: &mut Menu<'_, '_, Message, Renderer>,
1081    delta: mouse::ScrollDelta,
1082    overlay_cursor: Point,
1083    viewport_size: Size,
1084    overlay_offset: Vector,
1085) -> event::Status
1086where
1087    Renderer: renderer::Renderer,
1088{
1089    use event::Status::{Captured, Ignored};
1090    use mouse::ScrollDelta;
1091
1092    let state = menu.tree.state.downcast_mut::<MenuBarState>();
1093
1094    let delta_y = match delta {
1095        ScrollDelta::Lines { y, .. } => y * 60.0,
1096        ScrollDelta::Pixels { y, .. } => y,
1097    };
1098
1099    let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) {
1100        // viewport space children bounds
1101        let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset;
1102
1103        let max_offset = (0.0 - children_bounds.y).max(0.0);
1104        let min_offset =
1105            (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0);
1106        (max_offset, min_offset)
1107    };
1108
1109    // update
1110    if state.menu_states.is_empty() {
1111        return Ignored;
1112    } else if state.menu_states.len() == 1 {
1113        let last_ms = &mut state.menu_states[0];
1114
1115        if last_ms.index.is_none() {
1116            return Captured;
1117        }
1118
1119        let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size);
1120        last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset);
1121    } else {
1122        // >= 2
1123        let max_index = state.menu_states.len() - 1;
1124        let last_two = &mut state.menu_states[max_index - 1..=max_index];
1125
1126        if last_two[1].index.is_some() {
1127            // scroll the last one
1128            let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size);
1129            last_two[1].scroll_offset =
1130                (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset);
1131        } else {
1132            if !last_two[0]
1133                .menu_bounds
1134                .children_bounds
1135                .contains(overlay_cursor)
1136            {
1137                return Captured;
1138            }
1139
1140            // scroll the second last one
1141            let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size);
1142            let scroll_offset = (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset);
1143            let clamped_delta_y = scroll_offset - last_two[0].scroll_offset;
1144            last_two[0].scroll_offset = scroll_offset;
1145
1146            // update the bounds of the last one
1147            last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y;
1148            last_two[1].menu_bounds.children_bounds.y += clamped_delta_y;
1149            last_two[1].menu_bounds.check_bounds.y += clamped_delta_y;
1150        }
1151    }
1152    Captured
1153}
1154
1155#[allow(clippy::pedantic)]
1156/// Returns (children_size, child_positions, child_sizes)
1157fn get_children_layout<Message, Renderer>(
1158    menu_tree: &MenuTree<'_, Message, Renderer>,
1159    renderer: &Renderer,
1160    item_width: ItemWidth,
1161    item_height: ItemHeight,
1162    tree: &mut [Tree],
1163) -> (Size, Vec<f32>, Vec<Size>)
1164where
1165    Renderer: renderer::Renderer,
1166{
1167    let width = match item_width {
1168        ItemWidth::Uniform(u) => f32::from(u),
1169        ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)),
1170    };
1171
1172    let child_sizes: Vec<Size> = match item_height {
1173        ItemHeight::Uniform(u) => {
1174            let count = menu_tree.children.len();
1175            (0..count).map(|_| Size::new(width, f32::from(u))).collect()
1176        }
1177        ItemHeight::Static(s) => menu_tree
1178            .children
1179            .iter()
1180            .map(|mt| Size::new(width, f32::from(mt.height.unwrap_or(s))))
1181            .collect(),
1182        ItemHeight::Dynamic(d) => menu_tree
1183            .children
1184            .iter()
1185            .map(|mt| {
1186                let w = mt.item.as_widget();
1187                match w.size().height {
1188                    Length::Fixed(f) => Size::new(width, f),
1189                    Length::Shrink => {
1190                        let l_height = w
1191                            .layout(
1192                                &mut tree[mt.index],
1193                                renderer,
1194                                &Limits::new(Size::ZERO, Size::new(width, f32::MAX)),
1195                            )
1196                            .size()
1197                            .height;
1198
1199                        let height = if (f32::MAX - l_height) < 0.001 {
1200                            f32::from(d)
1201                        } else {
1202                            l_height
1203                        };
1204
1205                        Size::new(width, height)
1206                    }
1207                    _ => mt.height.map_or_else(
1208                        || Size::new(width, f32::from(d)),
1209                        |h| Size::new(width, f32::from(h)),
1210                    ),
1211                }
1212            })
1213            .collect(),
1214    };
1215
1216    let max_index = menu_tree.children.len() - 1;
1217    let child_positions: Vec<f32> = std::iter::once(0.0)
1218        .chain(child_sizes[0..max_index].iter().scan(0.0, |acc, x| {
1219            *acc += x.height;
1220            Some(*acc)
1221        }))
1222        .collect();
1223
1224    let height = child_sizes.iter().fold(0.0, |acc, x| acc + x.height);
1225
1226    (Size::new(width, height), child_positions, child_sizes)
1227}
1228
1229fn search_bound(
1230    default: usize,
1231    default_left: usize,
1232    default_right: usize,
1233    bound: f32,
1234    positions: &[f32],
1235    sizes: &[Size],
1236) -> usize {
1237    // binary search
1238    let mut left = default_left;
1239    let mut right = default_right;
1240
1241    let mut index = default;
1242    while left != right {
1243        let m = ((left + right) / 2) + 1;
1244        if positions[m] > bound {
1245            right = m - 1;
1246        } else {
1247            left = m;
1248        }
1249    }
1250    // let height = f32::from(menu_tree.children[left].height.unwrap_or(default_height));
1251    let height = sizes[left].height;
1252    if positions[left] + height > bound {
1253        index = left;
1254    }
1255    index
1256}