Skip to main content

cosmic/widget/menu/
menu_inner.rs

1// From iced_aw, license MIT
2
3//! Menu tree overlay
4use std::borrow::Cow;
5use std::sync::Arc;
6
7use super::menu_bar::MenuBarState;
8use super::menu_tree::MenuTree;
9#[cfg(wayland_platform)]
10use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem};
11use crate::style::menu_bar::StyleSheet;
12
13use iced::{Alignment, window};
14use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget};
15use iced_widget::core::layout::{Limits, Node};
16use iced_widget::core::mouse::{self, Cursor};
17use iced_widget::core::widget::Tree;
18use iced_widget::core::{
19    Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, overlay,
20    renderer, touch,
21};
22
23/// The condition of when to close a menu
24#[derive(Debug, Clone, Copy)]
25pub struct CloseCondition {
26    /// Close menus when the cursor moves outside the check bounds
27    pub leave: bool,
28
29    /// Close menus when the cursor clicks outside the check bounds
30    pub click_outside: bool,
31
32    /// Close menus when the cursor clicks inside the check bounds
33    pub click_inside: bool,
34}
35
36/// The width of an item
37#[derive(Debug, Clone, Copy)]
38pub enum ItemWidth {
39    /// Use uniform width
40    Uniform(u16),
41    /// Static tries to use the width value of each menu(menu tree with children),
42    /// the widths of items(menu tree with empty children) will be the same as the menu they're in,
43    /// if that value is None,
44    /// the default value will be used instead,
45    /// which is the value of the Static variant
46    Static(u16),
47}
48
49/// The height of an item
50#[derive(Debug, Clone, Copy)]
51pub enum ItemHeight {
52    /// Use uniform height.
53    Uniform(u16),
54    /// Static tries to use `MenuTree.height` as item height,
55    /// when it's `None` it'll fallback to the value of the `Static` variant.
56    Static(u16),
57    /// Dynamic tries to automatically choose the proper item height for you,
58    /// but it only works in certain cases:
59    ///
60    /// - Fixed height
61    /// - Shrink height
62    /// - Menu tree height
63    ///
64    /// If none of these is the case, it'll fallback to the value of the `Dynamic` variant.
65    Dynamic(u16),
66}
67
68/// Methods for drawing path highlight
69#[derive(Debug, Clone, Copy)]
70pub enum PathHighlight {
71    /// Draw the full path,
72    Full,
73    /// Omit the active item(the last item in the path)
74    OmitActive,
75    /// Omit the active item if it's not a menu
76    MenuActive,
77}
78
79/// X+ goes right and Y+ goes down
80#[derive(Debug, Clone, Copy)]
81pub(crate) enum Direction {
82    Positive,
83    Negative,
84}
85
86/// Adaptive open direction
87#[derive(Debug)]
88#[allow(clippy::struct_excessive_bools)]
89struct Aod {
90    // whether or not to use aod
91    horizontal: bool,
92    vertical: bool,
93
94    // whether or not to use overlap
95    horizontal_overlap: bool,
96    vertical_overlap: bool,
97
98    // default direction
99    horizontal_direction: Direction,
100    vertical_direction: Direction,
101
102    // Offset of the child in the default direction
103    horizontal_offset: f32,
104    vertical_offset: f32,
105}
106impl Aod {
107    /// Returns child position and offset position
108    #[allow(clippy::too_many_arguments)]
109    fn adaptive(
110        parent_pos: f32,
111        parent_size: f32,
112        child_size: f32,
113        max_size: f32,
114        offset: f32,
115        on: bool,
116        overlap: bool,
117        direction: Direction,
118    ) -> (f32, f32) {
119        /*
120        Imagine there're two sticks, parent and child
121        parent: o-----o
122        child:  o----------o
123
124        Now we align the child to the parent in one dimension
125        There are 4 possibilities:
126
127        1. to the right
128                    o-----oo----------o
129
130        2. to the right but allow overlaping
131                    o-----o
132                    o----------o
133
134        3. to the left
135        o----------oo-----o
136
137        4. to the left but allow overlaping
138                    o-----o
139               o----------o
140
141        The child goes to the default direction by default,
142        if the space on the default direction runs out it goes to the the other,
143        whether to use overlap is the caller's decision
144
145        This can be applied to any direction
146        */
147
148        match direction {
149            Direction::Positive => {
150                let space_negative = parent_pos;
151                let space_positive = max_size - parent_pos - parent_size;
152
153                if overlap {
154                    let overshoot = child_size - parent_size;
155                    if on && space_negative > space_positive && overshoot > space_positive {
156                        (parent_pos - overshoot, parent_pos - overshoot)
157                    } else {
158                        (parent_pos, parent_pos)
159                    }
160                } else {
161                    let overshoot = child_size + offset;
162                    if on && space_negative > space_positive && overshoot > space_positive {
163                        (parent_pos - overshoot, parent_pos - offset)
164                    } else {
165                        (parent_pos + parent_size + offset, parent_pos + parent_size)
166                    }
167                }
168            }
169            Direction::Negative => {
170                let space_positive = parent_pos;
171                let space_negative = max_size - parent_pos - parent_size;
172
173                if overlap {
174                    let overshoot = child_size - parent_size;
175                    if on && space_negative > space_positive && overshoot > space_positive {
176                        (parent_pos, parent_pos)
177                    } else {
178                        (parent_pos - overshoot, parent_pos - overshoot)
179                    }
180                } else {
181                    let overshoot = child_size + offset;
182                    if on && space_negative > space_positive && overshoot > space_positive {
183                        (parent_pos + parent_size + offset, parent_pos + parent_size)
184                    } else {
185                        (parent_pos - overshoot, parent_pos - offset)
186                    }
187                }
188            }
189        }
190    }
191
192    /// Returns child position and offset position
193    fn resolve(
194        &self,
195        parent_bounds: Rectangle,
196        children_size: Size,
197        viewport_size: Size,
198    ) -> (Point, Point) {
199        let (x, ox) = Self::adaptive(
200            parent_bounds.x,
201            parent_bounds.width,
202            children_size.width,
203            viewport_size.width,
204            self.horizontal_offset,
205            self.horizontal,
206            self.horizontal_overlap,
207            self.horizontal_direction,
208        );
209        let (y, oy) = Self::adaptive(
210            parent_bounds.y,
211            parent_bounds.height,
212            children_size.height,
213            viewport_size.height,
214            self.vertical_offset,
215            self.vertical,
216            self.vertical_overlap,
217            self.vertical_direction,
218        );
219
220        ([x, y].into(), [ox, oy].into())
221    }
222}
223
224/// A part of a menu where items are displayed.
225///
226/// When the bounds of a menu exceed the viewport,
227/// only items inside the viewport will be displayed,
228/// when scrolling happens, this should be updated
229#[derive(Debug, Clone, Copy)]
230pub(super) struct MenuSlice {
231    pub(super) start_index: usize,
232    pub(super) end_index: usize,
233    pub(super) lower_bound_rel: f32,
234    pub(super) upper_bound_rel: f32,
235}
236
237#[derive(Debug, Clone)]
238/// Menu bounds in overlay space
239pub struct MenuBounds {
240    pub(crate) child_positions: Vec<f32>,
241    child_sizes: Vec<Size>,
242    children_bounds: Rectangle,
243    pub parent_bounds: Rectangle,
244    check_bounds: Rectangle,
245    offset_bounds: Rectangle,
246}
247impl MenuBounds {
248    #[allow(clippy::too_many_arguments)]
249    fn new<Message>(
250        menu_tree: &MenuTree<Message>,
251        renderer: &crate::Renderer,
252        item_width: ItemWidth,
253        item_height: ItemHeight,
254        viewport_size: Size,
255        overlay_offset: Vector,
256        aod: &Aod,
257        bounds_expand: u16,
258        parent_bounds: Rectangle,
259        tree: &mut [Tree],
260        is_overlay: bool,
261    ) -> Self {
262        let (children_size, child_positions, child_sizes) =
263            get_children_layout(menu_tree, renderer, item_width, item_height, tree);
264
265        // viewport space parent bounds
266        let view_parent_bounds = parent_bounds + overlay_offset;
267
268        // overlay space children position
269        let (children_position, offset_position) = {
270            let (cp, op) = aod.resolve(view_parent_bounds, children_size, viewport_size);
271            if is_overlay {
272                (cp - overlay_offset, op - overlay_offset)
273            } else {
274                (Point::ORIGIN, op - overlay_offset)
275            }
276        };
277
278        // calc offset bounds
279        let delta = children_position - offset_position;
280        let offset_size = if delta.x.abs() > delta.y.abs() {
281            Size::new(delta.x, children_size.height)
282        } else {
283            Size::new(children_size.width, delta.y)
284        };
285        let offset_bounds = Rectangle::new(offset_position, offset_size);
286
287        let children_bounds = Rectangle::new(children_position, children_size);
288        let check_bounds = pad_rectangle(children_bounds, bounds_expand.into());
289
290        Self {
291            child_positions,
292            child_sizes,
293            children_bounds,
294            parent_bounds,
295            check_bounds,
296            offset_bounds,
297        }
298    }
299}
300
301#[derive(Clone)]
302pub(crate) struct MenuState {
303    /// The index of the active menu item
304    pub(crate) index: Option<usize>,
305    scroll_offset: f32,
306    pub menu_bounds: MenuBounds,
307}
308impl MenuState {
309    pub(super) fn layout<Message>(
310        &mut self,
311        overlay_offset: Vector,
312        slice: MenuSlice,
313        renderer: &crate::Renderer,
314        menu_tree: &[MenuTree<Message>],
315        tree: &mut [Tree],
316    ) -> Node {
317        let MenuSlice {
318            start_index,
319            end_index,
320            lower_bound_rel,
321            upper_bound_rel,
322        } = slice;
323
324        debug_assert_eq!(menu_tree.len(), self.menu_bounds.child_positions.len());
325
326        // viewport space children bounds
327        let children_bounds = self.menu_bounds.children_bounds + overlay_offset;
328        let child_nodes = self.menu_bounds.child_positions[start_index..=end_index]
329            .iter_mut()
330            .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter_mut())
331            .zip(menu_tree[start_index..=end_index].iter())
332            .map(|((cp, size), mt)| {
333                let mut position = *cp;
334                let mut size = *size;
335
336                if position < lower_bound_rel && (position + size.height) > lower_bound_rel {
337                    size.height = position + size.height - lower_bound_rel;
338                    position = lower_bound_rel;
339                } else if position <= upper_bound_rel && (position + size.height) > upper_bound_rel
340                {
341                    size.height = upper_bound_rel - position;
342                }
343
344                let limits = Limits::new(size, size);
345
346                mt.item
347                    .element
348                    .with_data_mut(|e| {
349                        e.as_widget_mut()
350                            .layout(&mut tree[mt.index], renderer, &limits)
351                    })
352                    .move_to(Point::new(0.0, position + self.scroll_offset))
353            })
354            .collect::<Vec<_>>();
355
356        Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position())
357    }
358
359    fn layout_single<Message>(
360        &self,
361        overlay_offset: Vector,
362        index: usize,
363        renderer: &crate::Renderer,
364        menu_tree: &mut MenuTree<Message>,
365        tree: &mut Tree,
366    ) -> Node {
367        // viewport space children bounds
368        let children_bounds = self.menu_bounds.children_bounds + overlay_offset;
369
370        let position = self.menu_bounds.child_positions[index];
371        let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]);
372        let parent_offset = children_bounds.position() - Point::ORIGIN;
373        let node = menu_tree.item.layout(tree, renderer, &limits);
374        node.move_to(Point::new(
375            parent_offset.x,
376            parent_offset.y + position + self.scroll_offset,
377        ))
378    }
379
380    /// returns a slice of the menu items that are inside the viewport
381    pub(super) fn slice(
382        &self,
383        viewport_size: Size,
384        overlay_offset: Vector,
385        item_height: ItemHeight,
386    ) -> MenuSlice {
387        // viewport space children bounds
388        let children_bounds = self.menu_bounds.children_bounds + overlay_offset;
389
390        let max_index = self.menu_bounds.child_positions.len().saturating_sub(1);
391
392        // viewport space absolute bounds
393        let lower_bound = children_bounds.y.max(0.0);
394        let upper_bound = (children_bounds.y + children_bounds.height).min(viewport_size.height);
395
396        // menu space relative bounds
397        let lower_bound_rel = lower_bound - (children_bounds.y + self.scroll_offset);
398        let upper_bound_rel = upper_bound - (children_bounds.y + self.scroll_offset);
399
400        // index range
401        let (start_index, end_index) = match item_height {
402            ItemHeight::Uniform(u) => {
403                let start_index = (lower_bound_rel / f32::from(u)).floor() as usize;
404                let end_index = ((upper_bound_rel / f32::from(u)).floor() as usize).min(max_index);
405                (start_index, end_index)
406            }
407            ItemHeight::Static(_) | ItemHeight::Dynamic(_) => {
408                let positions = &self.menu_bounds.child_positions;
409                let sizes = &self.menu_bounds.child_sizes;
410
411                let start_index = search_bound(0, 0, max_index, lower_bound_rel, positions, sizes);
412                let end_index = search_bound(
413                    max_index,
414                    start_index,
415                    max_index,
416                    upper_bound_rel,
417                    positions,
418                    sizes,
419                )
420                .min(max_index);
421
422                (start_index, end_index)
423            }
424        };
425
426        MenuSlice {
427            start_index,
428            end_index,
429            lower_bound_rel,
430            upper_bound_rel,
431        }
432    }
433}
434
435#[derive(Clone)]
436pub(crate) struct Menu<'b, Message: std::clone::Clone> {
437    pub(crate) tree: MenuBarState,
438    // Flattened menu tree
439    pub(crate) menu_roots: Cow<'b, [MenuTree<Message>]>,
440    pub(crate) bounds_expand: u16,
441    /// Allows menu overlay items to overlap the parent
442    pub(crate) menu_overlays_parent: bool,
443    pub(crate) close_condition: CloseCondition,
444    pub(crate) item_width: ItemWidth,
445    pub(crate) item_height: ItemHeight,
446    pub(crate) bar_bounds: Rectangle,
447    pub(crate) main_offset: i32,
448    pub(crate) cross_offset: i32,
449    pub(crate) root_bounds_list: Vec<Rectangle>,
450    pub(crate) path_highlight: Option<PathHighlight>,
451    pub(crate) style: Cow<'b, <crate::Theme as StyleSheet>::Style>,
452    pub(crate) position: Point,
453    pub(crate) is_overlay: bool,
454    /// window id for this popup
455    pub(crate) window_id: window::Id,
456    pub(crate) depth: usize,
457    pub(crate) on_surface_action:
458        Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
459}
460impl<'b, Message: Clone + 'static> Menu<'b, Message> {
461    pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> {
462        overlay::Element::new(Box::new(self))
463    }
464
465    pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node {
466        // layout children;
467        let position = self.position;
468        let mut intrinsic_size = Size::ZERO;
469
470        let empty = Vec::new();
471        self.tree.inner.with_data_mut(|data| {
472            if data.active_root.len() < self.depth + 1 || data.menu_states.len() < self.depth + 1 {
473                return Node::new(limits.min());
474            }
475
476            let overlay_offset = Point::ORIGIN - position;
477            let tree_children: &mut Vec<Tree> = &mut data.tree.children;
478
479            let children = (if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay {
480                data.active_root.len() - 1
481            } else {
482                self.depth
483            })
484                .map(|active_root| {
485                    if self.menu_roots.is_empty() {
486                        return (&empty, vec![]);
487                    }
488                    let (active_tree, roots) =
489                        data.active_root[..=active_root].iter().skip(1).fold(
490                            (
491                                &mut tree_children[data.active_root[0]].children,
492                                &self.menu_roots[data.active_root[0]].children,
493                            ),
494                            |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children),
495                        );
496
497                    data.menu_states[if self.is_overlay { 0 } else { self.depth }
498                        ..=if self.is_overlay {
499                            data.active_root.len() - 1
500                        } else {
501                            self.depth
502                        }]
503                        .iter_mut()
504                        .enumerate()
505                        .filter(|ms| self.is_overlay || ms.0 < 1)
506                        .fold(
507                            (roots, Vec::new()),
508                            |(menu_root, mut nodes), (_i, ms)| {
509                                let slice =
510                                    ms.slice(limits.max(), overlay_offset, self.item_height);
511                                let _start_index = slice.start_index;
512                                let _end_index = slice.end_index;
513                                let children_node = ms.layout(
514                                    overlay_offset,
515                                    slice,
516                                    renderer,
517                                    menu_root,
518                                    active_tree,
519                                );
520                                let node_size = children_node.size();
521                                intrinsic_size.height += node_size.height;
522
523                                intrinsic_size.width = intrinsic_size.width.max(node_size.width);
524
525                                nodes.push(children_node);
526                                // if popup just use len 1?
527                                // only the last menu can have a None active index
528                                (
529                                    ms.index
530                                        .map_or(menu_root, |active| &menu_root[active].children),
531                                    nodes,
532                                )
533                            },
534                        )
535                })
536                .map(|(_, l)| l)
537                .next()
538                .unwrap_or_default();
539
540            // overlay space viewport rectangle
541            Node::with_children(
542                limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size),
543                children,
544            )
545            .translate(Point::ORIGIN - position)
546        })
547    }
548
549    #[allow(clippy::too_many_lines)]
550    fn update(
551        &mut self,
552        event: &event::Event,
553        layout: Layout<'_>,
554        view_cursor: Cursor,
555        renderer: &crate::Renderer,
556        clipboard: &mut dyn Clipboard,
557        shell: &mut Shell<'_, Message>,
558    ) -> Option<(usize, MenuState)> {
559        use event::Event::{Mouse, Touch};
560        use event::Status::{Captured, Ignored};
561        use mouse::Button::Left;
562        use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled};
563        use touch::Event::{FingerLifted, FingerMoved, FingerPressed};
564
565        if !self
566            .tree
567            .inner
568            .with_data(|data| data.open || data.active_root.len() <= self.depth)
569        {
570            return None;
571        }
572
573        let viewport = layout.bounds();
574
575        let viewport_size = viewport.size();
576        let overlay_offset = Point::ORIGIN - viewport.position();
577        let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
578        let menu_roots = match &mut self.menu_roots {
579            Cow::Borrowed(_) => panic!(),
580            Cow::Owned(o) => o.as_mut_slice(),
581        };
582        process_menu_events(
583            self,
584            event,
585            view_cursor,
586            renderer,
587            clipboard,
588            shell,
589            overlay_offset,
590        );
591
592        init_root_menu(
593            self,
594            renderer,
595            shell,
596            overlay_cursor,
597            viewport_size,
598            overlay_offset,
599            self.bar_bounds,
600            self.main_offset as f32,
601        );
602
603        match event {
604            Mouse(WheelScrolled { delta }) => process_scroll_events(
605                self,
606                shell,
607                *delta,
608                overlay_cursor,
609                viewport_size,
610                overlay_offset,
611            ),
612
613            Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => {
614                self.tree.inner.with_data_mut(|data| {
615                    data.pressed = true;
616                    data.view_cursor = view_cursor;
617                });
618            }
619
620            Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => {
621                let view_cursor = Cursor::Available(*position);
622                let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset;
623                if !self.is_overlay && !view_cursor.is_over(viewport) {
624                    return None;
625                }
626                let new_root = process_overlay_events(
627                    self,
628                    renderer,
629                    viewport_size,
630                    overlay_offset,
631                    view_cursor,
632                    overlay_cursor,
633                    self.cross_offset as f32,
634                    shell,
635                );
636
637                if view_cursor.is_over(viewport) {
638                    shell.request_redraw();
639                }
640
641                if self.is_overlay && view_cursor.is_over(viewport) {
642                    shell.capture_event();
643                }
644
645                return new_root;
646            }
647
648            Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => {
649                self.tree.inner.with_data_mut(|state| {
650                    state.pressed = false;
651
652                    // process close condition
653                    if state.open
654                        && state
655                            .view_cursor
656                            .position()
657                            .unwrap_or_default()
658                            .distance(view_cursor.position().unwrap_or_default())
659                            < 2.0
660                    {
661                        let is_inside = state.menu_states[..=if self.is_overlay {
662                            state.active_root.len().saturating_sub(1)
663                        } else {
664                            self.depth
665                        }]
666                            .iter()
667                            .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor));
668                        let mut needs_reset = false;
669                        needs_reset |= self.close_condition.click_inside
670                            && is_inside
671                            && matches!(
672                                event,
673                                Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. })
674                            );
675
676                        needs_reset |= self.close_condition.click_outside && !is_inside;
677
678                        if needs_reset {
679                            #[cfg(wayland_platform)]
680                            if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
681                                && let Some(handler) = self.on_surface_action.as_ref()
682                            {
683                                let mut root = self.window_id;
684                                let mut depth = self.depth;
685                                while let Some(parent) =
686                                    state.popup_id.iter().find(|(_, v)| **v == root)
687                                {
688                                    // parent of root popup is the window, so we stop.
689                                    if depth == 0 {
690                                        break;
691                                    }
692                                    root = *parent.0;
693                                    depth = depth.saturating_sub(1);
694                                }
695                                shell
696                                    .publish((handler)(crate::surface::Action::DestroyPopup(root)));
697                            }
698
699                            state.reset();
700                        }
701                    }
702
703                    // close all menus when clicking inside the menu bar
704                    if self.bar_bounds.contains(overlay_cursor) {
705                        state.reset();
706                    }
707                });
708            }
709
710            _ => {}
711        }
712        None
713    }
714
715    #[allow(unused_results, clippy::too_many_lines)]
716    fn draw(
717        &self,
718        renderer: &mut crate::Renderer,
719        theme: &crate::Theme,
720        style: &renderer::Style,
721        layout: Layout<'_>,
722        view_cursor: Cursor,
723    ) {
724        self.tree.inner.with_data(|state| {
725            if !state.open || state.active_root.len() <= self.depth {
726                return;
727            }
728            let active_root = &state.active_root[..=if self.is_overlay { 0 } else { self.depth }];
729            let viewport = layout.bounds();
730            let viewport_size = viewport.size();
731            let overlay_offset = Point::ORIGIN - viewport.position();
732
733            let render_bounds = if self.is_overlay {
734                Rectangle::new(Point::ORIGIN, viewport.size())
735            } else {
736                Rectangle::new(Point::ORIGIN, Size::INFINITE)
737            };
738
739            let styling = theme.appearance(&self.style, self.is_overlay);
740            let roots = active_root.iter().skip(1).fold(
741                &self.menu_roots[active_root[0]].children,
742                |mt, next_active_root| &mt[*next_active_root].children,
743            );
744            let indices = state.get_trimmed_indices(self.depth).collect::<Vec<_>>();
745            state.menu_states[if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay {
746                state.menu_states.len() - 1
747            } else {
748                self.depth
749            }]
750                .iter()
751                .zip(layout.children())
752                .enumerate()
753                .filter(|ms: &(usize, (&MenuState, Layout<'_>))| self.is_overlay || ms.0 < 1)
754                .fold(
755                    roots,
756                    |menu_roots: &Vec<MenuTree<Message>>, (i, (ms, children_layout))| {
757                        let draw_path = self.path_highlight.as_ref().is_some_and(|ph| match ph {
758                            PathHighlight::Full => true,
759                            PathHighlight::OmitActive => {
760                                !indices.is_empty() && i < indices.len() - 1
761                            }
762                            PathHighlight::MenuActive => {
763                                !indices.is_empty()
764                                    && i < indices.len()
765                                    && menu_roots.len() > indices[i]
766                                    && (i < indices.len() - 1
767                                        || !menu_roots[indices[i]].children.is_empty())
768                            }
769                        });
770
771                        // react only to the last menu
772                        let view_cursor = if self.depth == state.active_root.len() - 1
773                            || i == state.menu_states.len() - 1
774                        {
775                            view_cursor
776                        } else {
777                            Cursor::Available([-1.0; 2].into())
778                        };
779
780                        let draw_menu = |r: &mut crate::Renderer| {
781                            // calc slice
782                            let slice = ms.slice(viewport_size, overlay_offset, self.item_height);
783                            let start_index = slice.start_index;
784                            let end_index = slice.end_index;
785
786                            let children_bounds = children_layout.bounds();
787
788                            // draw menu background
789                            // let bounds = pad_rectangle(children_bounds, styling.background_expand.into());
790                            // println!("cursor: {:?}", view_cursor);
791                            // println!("bg_bounds: {:?}", bounds);
792                            // println!("color: {:?}\n", styling.background);
793                            let menu_quad = renderer::Quad {
794                                bounds: pad_rectangle(
795                                    children_bounds.intersection(&viewport).unwrap_or_default(),
796                                    styling.background_expand.into(),
797                                ),
798                                border: Border {
799                                    radius: styling.menu_border_radius.into(),
800                                    width: styling.border_width,
801                                    color: styling.border_color,
802                                },
803                                shadow: Shadow::default(),
804                                snap: true,
805                            };
806                            let menu_color = styling.background;
807                            r.fill_quad(menu_quad, menu_color);
808                            // draw path hightlight
809                            if let (true, Some(active)) = (draw_path, ms.index)
810                                && let Some(active_layout) = children_layout
811                                    .children()
812                                    .nth(active.saturating_sub(start_index))
813                            {
814                                let i = active.saturating_sub(start_index);
815                                let mut rad = styling.menu_border_radius;
816                                let rad_0 = theme.cosmic().radius_0();
817                                if start_index != end_index {
818                                    if 0 == i {
819                                        rad[2] = rad_0[0];
820                                        rad[3] = rad_0[1];
821                                    } else if i == end_index - start_index {
822                                        rad[0] = rad_0[2];
823                                        rad[1] = rad_0[3];
824                                    } else {
825                                        rad = rad_0;
826                                    }
827                                }
828                                let path_quad = renderer::Quad {
829                                    bounds: active_layout
830                                        .bounds()
831                                        .intersection(&viewport)
832                                        .unwrap_or_default(),
833                                    border: Border {
834                                        radius: rad.into(),
835                                        ..Default::default()
836                                    },
837                                    shadow: Shadow::default(),
838                                    snap: true,
839                                };
840
841                                r.fill_quad(path_quad, styling.path);
842                            }
843                            if start_index < menu_roots.len() {
844                                // draw item
845                                menu_roots[start_index..=end_index]
846                                    .iter()
847                                    .enumerate()
848                                    .zip(children_layout.children())
849                                    .for_each(|((i, mt), clo)| {
850                                        let t = theme.with_list_item_position(
851                                            if start_index == end_index {
852                                                Some((Alignment::Center, i))
853                                            } else if 0 == i {
854                                                Some((Alignment::Start, i))
855                                            } else if i == end_index - start_index {
856                                                Some((Alignment::End, i))
857                                            } else {
858                                                None
859                                            },
860                                        );
861
862                                        mt.item.draw(
863                                            &state.tree.children[active_root[0]].children[mt.index],
864                                            r,
865                                            &t,
866                                            style,
867                                            clo,
868                                            view_cursor,
869                                            &children_layout
870                                                .bounds()
871                                                .intersection(&viewport)
872                                                .unwrap_or_default(),
873                                        );
874                                    });
875                            }
876                        };
877
878                        renderer.with_layer(render_bounds, draw_menu);
879
880                        // only the last menu can have a None active index
881                        ms.index
882                            .map_or(menu_roots, |active| &menu_roots[active].children)
883                    },
884                );
885        });
886    }
887}
888impl<Message: Clone + 'static> overlay::Overlay<Message, crate::Theme, crate::Renderer>
889    for Menu<'_, Message>
890{
891    fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node {
892        Menu::layout(
893            self,
894            renderer,
895            Limits::NONE
896                .min_width(bounds.width)
897                .max_width(bounds.width)
898                .min_height(bounds.height)
899                .max_height(bounds.height),
900        )
901    }
902
903    fn update(
904        &mut self,
905        event: &iced::Event,
906        layout: Layout<'_>,
907        cursor: mouse::Cursor,
908        renderer: &crate::Renderer,
909        clipboard: &mut dyn Clipboard,
910        shell: &mut Shell<'_, Message>,
911    ) {
912        self.update(event, layout, cursor, renderer, clipboard, shell);
913    }
914
915    fn draw(
916        &self,
917        renderer: &mut crate::Renderer,
918        theme: &crate::Theme,
919        style: &renderer::Style,
920        layout: Layout<'_>,
921        cursor: mouse::Cursor,
922    ) {
923        self.draw(renderer, theme, style, layout, cursor);
924    }
925
926    fn mouse_interaction(
927        &self,
928        layout: Layout<'_>,
929        cursor: mouse::Cursor,
930        _renderer: &crate::Renderer,
931    ) -> mouse::Interaction {
932        if cursor.is_over(layout.bounds()) {
933            mouse::Interaction::Idle
934        } else {
935            mouse::Interaction::None
936        }
937    }
938}
939
940impl<Message: std::clone::Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
941    for Menu<'_, Message>
942{
943    fn size(&self) -> Size<Length> {
944        Size {
945            width: Length::Shrink,
946            height: Length::Shrink,
947        }
948    }
949
950    fn layout(
951        &mut self,
952        _tree: &mut Tree,
953        renderer: &crate::Renderer,
954        limits: &iced_core::layout::Limits,
955    ) -> iced_core::layout::Node {
956        Menu::layout(self, renderer, *limits)
957    }
958
959    fn draw(
960        &self,
961        _tree: &Tree,
962        renderer: &mut crate::Renderer,
963        theme: &crate::Theme,
964        style: &renderer::Style,
965        layout: Layout<'_>,
966        cursor: mouse::Cursor,
967        _viewport: &Rectangle,
968    ) {
969        Menu::draw(self, renderer, theme, style, layout, cursor);
970    }
971
972    #[allow(clippy::too_many_lines)]
973    fn update(
974        &mut self,
975        tree: &mut Tree,
976        event: &iced::Event,
977        layout: Layout<'_>,
978        cursor: mouse::Cursor,
979        renderer: &crate::Renderer,
980        clipboard: &mut dyn Clipboard,
981        shell: &mut Shell<'_, Message>,
982        viewport: &Rectangle,
983    ) {
984        let new_root = self.update(event, layout, cursor, renderer, clipboard, shell);
985
986        #[cfg(wayland_platform)]
987        if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
988            && let Some((new_root, new_ms)) = new_root
989        {
990            use iced_runtime::platform_specific::wayland::CornerRadius;
991            use iced_runtime::platform_specific::wayland::popup::{
992                SctkPopupSettings, SctkPositioner,
993            };
994
995            use crate::surface::action::LiveSettings;
996            use crate::theme::THEME;
997            let overlay_offset = Point::ORIGIN - viewport.position();
998
999            let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset;
1000
1001            let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| {
1002                let popup_id = *state
1003                    .popup_id
1004                    .entry(self.window_id)
1005                    .or_insert_with(window::Id::unique);
1006                let active_roots = state
1007                    .active_root
1008                    .get(self.depth)
1009                    .cloned()
1010                    .unwrap_or_default();
1011
1012                let root_bounds_list = layout
1013                    .children()
1014                    .next()
1015                    .unwrap()
1016                    .children()
1017                    .map(|lo| lo.bounds())
1018                    .collect();
1019
1020                let mut popup_menu = Menu {
1021                    tree: self.tree.clone(),
1022                    menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())),
1023                    bounds_expand: self.bounds_expand,
1024                    menu_overlays_parent: false,
1025                    close_condition: self.close_condition,
1026                    item_width: self.item_width,
1027                    item_height: self.item_height,
1028                    bar_bounds: layout.bounds(),
1029                    main_offset: self.main_offset,
1030                    cross_offset: self.cross_offset,
1031                    root_bounds_list,
1032                    path_highlight: self.path_highlight,
1033                    style: Cow::Owned(Cow::into_owned(self.style.clone())),
1034                    position: Point::new(0., 0.),
1035                    is_overlay: false,
1036                    window_id: popup_id,
1037                    depth: self.depth + 1,
1038                    on_surface_action: self.on_surface_action.clone(),
1039                };
1040
1041                state.active_root.push(new_root);
1042
1043                Some((popup_menu, popup_id))
1044            }) else {
1045                return;
1046            };
1047            // XXX we push a new active root manually instead
1048            init_root_popup_menu(
1049                &mut menu,
1050                renderer,
1051                shell,
1052                cursor.position().unwrap_or_default(),
1053                layout.bounds().size(),
1054                Vector::new(0., 0.),
1055                layout.bounds(),
1056                self.main_offset as f32,
1057            );
1058            let (anchor_rect, gravity) = self.tree.inner.with_data_mut(|state| {
1059                (state
1060                    .menu_states
1061                    .get(self.depth + 1)
1062                    .map(|s| s.menu_bounds.parent_bounds)
1063                    .map_or_else(
1064                        || {
1065                            let bounds = layout.bounds();
1066                            Rectangle {
1067                                x: bounds.x as i32,
1068                                y: bounds.y as i32,
1069                                width: bounds.width as i32,
1070                                height: bounds.height as i32,
1071                            }
1072                        },
1073                        |r| Rectangle {
1074                            x: r.x as i32,
1075                            y: r.y as i32,
1076                            width: r.width as i32,
1077                            height: r.height as i32,
1078                        },
1079                    ), match (state.horizontal_direction, state.vertical_direction) {
1080                        (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
1081                        (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight,
1082                        (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft,
1083                        (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft,
1084                    })
1085            });
1086
1087            let menu_node = Widget::layout(
1088                &mut menu,
1089                &mut Tree::empty(),
1090                renderer,
1091                &Limits::NONE.min_width(1.).min_height(1.),
1092            );
1093
1094            let popup_size = menu_node.size();
1095            let mut positioner = SctkPositioner {
1096                size: Some((
1097                    popup_size.width.ceil() as u32 + 2,
1098                    popup_size.height.ceil() as u32 + 2,
1099                )),
1100                anchor_rect,
1101                anchor:
1102                    cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::TopRight,
1103                gravity,
1104                reactive: true,
1105                ..Default::default()
1106            };
1107            // disable slide_x if it is set in the default
1108            positioner.constraint_adjustment &= !(1 << 0);
1109            let parent = self.window_id;
1110
1111            let t = THEME.lock().unwrap();
1112            let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false);
1113            drop(t);
1114            let rad = styling.menu_border_radius;
1115
1116            shell.publish((self.on_surface_action.as_ref().unwrap())(
1117                crate::surface::action::simple_popup(
1118                    move || LiveSettings {
1119                        corners: Some(CornerRadius {
1120                            top_left: rad[0] as u32,
1121                            top_right: rad[1] as u32,
1122                            bottom_left: rad[2] as u32,
1123                            bottom_right: rad[3] as u32,
1124                        }),
1125                        ..Default::default()
1126                    },
1127                    move || SctkPopupSettings {
1128                        parent,
1129                        id: popup_id,
1130                        positioner: positioner.clone(),
1131                        parent_size: None,
1132                        grab: true,
1133                        close_with_children: false,
1134                        input_zone: None,
1135                    },
1136                    Some(move || {
1137                        crate::Element::from(
1138                            crate::widget::container(menu.clone()).center(Length::Fill),
1139                        )
1140                        .map(crate::action::app)
1141                    }),
1142                ),
1143            ));
1144        }
1145    }
1146
1147    fn mouse_interaction(
1148        &self,
1149        _tree: &Tree,
1150        layout: Layout<'_>,
1151        cursor: mouse::Cursor,
1152        _viewport: &Rectangle,
1153        _renderer: &crate::Renderer,
1154    ) -> mouse::Interaction {
1155        if cursor.is_over(layout.bounds()) {
1156            mouse::Interaction::Idle
1157        } else {
1158            mouse::Interaction::None
1159        }
1160    }
1161}
1162
1163impl<'a, Message> From<Menu<'a, Message>>
1164    for iced::Element<'a, Message, crate::Theme, crate::Renderer>
1165where
1166    Message: std::clone::Clone + 'static,
1167{
1168    fn from(value: Menu<'a, Message>) -> Self {
1169        Self::new(value)
1170    }
1171}
1172
1173fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle {
1174    Rectangle {
1175        x: rect.x - padding.left,
1176        y: rect.y - padding.top,
1177        width: rect.width + padding.x(),
1178        height: rect.height + padding.y(),
1179    }
1180}
1181
1182#[allow(clippy::too_many_arguments)]
1183pub(crate) fn init_root_menu<Message: Clone>(
1184    menu: &mut Menu<'_, Message>,
1185    renderer: &crate::Renderer,
1186    shell: &mut Shell<'_, Message>,
1187    overlay_cursor: Point,
1188    viewport_size: Size,
1189    overlay_offset: Vector,
1190    bar_bounds: Rectangle,
1191    main_offset: f32,
1192) {
1193    menu.tree.inner.with_data_mut(|state| {
1194        if !(state.menu_states.get(menu.depth).is_none()
1195            && (!menu.is_overlay || bar_bounds.contains(overlay_cursor)))
1196            || menu.depth > 0
1197            || !state.open
1198        {
1199            return;
1200        }
1201
1202        for (i, (&root_bounds, mt)) in menu
1203            .root_bounds_list
1204            .iter()
1205            .zip(menu.menu_roots.iter())
1206            .enumerate()
1207        {
1208            if mt.children.is_empty() {
1209                continue;
1210            }
1211
1212            if root_bounds.contains(overlay_cursor) {
1213                let view_center = viewport_size.width * 0.5;
1214                let rb_center = root_bounds.center_x();
1215
1216                state.horizontal_direction = if menu.is_overlay && rb_center > view_center {
1217                    Direction::Negative
1218                } else {
1219                    Direction::Positive
1220                };
1221
1222                let aod = Aod {
1223                    horizontal: true,
1224                    vertical: true,
1225                    horizontal_overlap: true,
1226                    vertical_overlap: false,
1227                    horizontal_direction: state.horizontal_direction,
1228                    vertical_direction: state.vertical_direction,
1229                    horizontal_offset: 0.0,
1230                    vertical_offset: main_offset,
1231                };
1232                let menu_bounds = MenuBounds::new(
1233                    mt,
1234                    renderer,
1235                    menu.item_width,
1236                    menu.item_height,
1237                    viewport_size,
1238                    overlay_offset,
1239                    &aod,
1240                    menu.bounds_expand,
1241                    root_bounds,
1242                    &mut state.tree.children[0].children,
1243                    menu.is_overlay,
1244                );
1245                state.active_root.push(i);
1246                let ms = MenuState {
1247                    index: None,
1248                    scroll_offset: 0.0,
1249                    menu_bounds,
1250                };
1251                state.menu_states.push(ms);
1252                // Hack to ensure menu opens properly
1253                shell.invalidate_layout();
1254
1255                break;
1256            }
1257        }
1258    });
1259}
1260
1261#[cfg(wayland_platform)]
1262pub(super) fn init_root_popup_menu<Message>(
1263    menu: &mut Menu<'_, Message>,
1264    renderer: &crate::Renderer,
1265    shell: &mut Shell<'_, Message>,
1266    overlay_cursor: Point,
1267    viewport_size: Size,
1268    overlay_offset: Vector,
1269    bar_bounds: Rectangle,
1270    main_offset: f32,
1271) where
1272    Message: std::clone::Clone,
1273{
1274    menu.tree.inner.with_data_mut(|state| {
1275        if !(state.menu_states.get(menu.depth).is_none()
1276            && (!menu.is_overlay || bar_bounds.contains(overlay_cursor)))
1277        {
1278            return;
1279        }
1280
1281        let active_roots = &state.active_root[..=menu.depth];
1282
1283        let mt = active_roots
1284            .iter()
1285            .skip(1)
1286            .fold(&menu.menu_roots[active_roots[0]], |mt, next_active_root| {
1287                &mt.children[*next_active_root]
1288            });
1289        let i = active_roots.last().unwrap();
1290        let root_bounds = menu.root_bounds_list[*i];
1291
1292        assert!(!mt.children.is_empty(), "skipping menu with no children");
1293        let aod = Aod {
1294            horizontal: true,
1295            vertical: true,
1296            horizontal_overlap: true,
1297            vertical_overlap: false,
1298            horizontal_direction: state.horizontal_direction,
1299            vertical_direction: state.vertical_direction,
1300            horizontal_offset: 0.0,
1301            vertical_offset: main_offset,
1302        };
1303        let menu_bounds = MenuBounds::new(
1304            mt,
1305            renderer,
1306            menu.item_width,
1307            menu.item_height,
1308            viewport_size,
1309            overlay_offset,
1310            &aod,
1311            menu.bounds_expand,
1312            root_bounds,
1313            // TODO how to select the tree for the popup
1314            &mut state.tree.children[0].children,
1315            menu.is_overlay,
1316        );
1317
1318        let view_center = viewport_size.width * 0.5;
1319        let rb_center = root_bounds.center_x();
1320
1321        state.horizontal_direction = if rb_center > view_center {
1322            Direction::Negative
1323        } else {
1324            Direction::Positive
1325        };
1326
1327        let ms = MenuState {
1328            index: None,
1329            scroll_offset: 0.0,
1330            menu_bounds,
1331        };
1332        state.menu_states.push(ms);
1333
1334        // Hack to ensure menu opens properly
1335        shell.invalidate_layout();
1336    });
1337}
1338
1339#[allow(clippy::too_many_arguments)]
1340fn process_menu_events<Message: std::clone::Clone>(
1341    menu: &mut Menu<Message>,
1342    event: &event::Event,
1343    view_cursor: Cursor,
1344    renderer: &crate::Renderer,
1345    clipboard: &mut dyn Clipboard,
1346    shell: &mut Shell<'_, Message>,
1347    overlay_offset: Vector,
1348) {
1349    let my_state = &mut menu.tree;
1350    let menu_roots = match &mut menu.menu_roots {
1351        Cow::Borrowed(_) => panic!(),
1352        Cow::Owned(o) => o.as_mut_slice(),
1353    };
1354    my_state.inner.with_data_mut(|state| {
1355        if state.active_root.len() <= menu.depth {
1356            return;
1357        }
1358
1359        let Some(hover) = state.menu_states.last_mut() else {
1360            return;
1361        };
1362
1363        let Some(hover_index) = hover.index else {
1364            return;
1365        };
1366
1367        let mt = state.active_root.iter().skip(1).fold(
1368            // then use menu states for each open menu
1369            &mut menu_roots[state.active_root[0]],
1370            |mt, next_active_root| &mut mt.children[*next_active_root],
1371        );
1372
1373        let mt = &mut mt.children[hover_index];
1374        let tree = &mut state.tree.children[state.active_root[0]].children[mt.index];
1375
1376        // get layout
1377        let child_node = hover.layout_single(
1378            overlay_offset,
1379            hover.index.expect("missing index within menu state."),
1380            renderer,
1381            mt,
1382            tree,
1383        );
1384        let child_layout = Layout::new(&child_node);
1385
1386        // process only the last widget
1387        mt.item.update(
1388            tree,
1389            event,
1390            child_layout,
1391            view_cursor,
1392            renderer,
1393            clipboard,
1394            shell,
1395            &Rectangle::default(),
1396        );
1397    });
1398}
1399
1400#[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)]
1401fn process_overlay_events<Message>(
1402    menu: &mut Menu<Message>,
1403    renderer: &crate::Renderer,
1404    viewport_size: Size,
1405    overlay_offset: Vector,
1406    view_cursor: Cursor,
1407    overlay_cursor: Point,
1408    cross_offset: f32,
1409    shell: &mut Shell<'_, Message>,
1410) -> Option<(usize, MenuState)>
1411where
1412    Message: std::clone::Clone,
1413{
1414    /*
1415    if no active root || pressed:
1416        return
1417    else:
1418        remove invalid menus // overlay space
1419        update active item
1420        if active item is a menu:
1421            add menu // viewport space
1422    */
1423    let mut new_menu_root = None;
1424
1425    menu.tree.inner.with_data_mut(|state| {
1426
1427        /* When overlay is running, cursor_position in any widget method will go negative
1428        but I still want Widget::draw() to react to cursor movement */
1429        state.view_cursor = view_cursor;
1430
1431        // * remove invalid menus
1432        if state.open {
1433            let mut prev_bounds = std::iter::once(menu.bar_bounds)
1434                .chain(
1435                    if menu.is_overlay {
1436                        state.menu_states[..state.menu_states.len().saturating_sub(1)].iter()
1437                    } else {
1438                        state.menu_states[..menu.depth].iter()
1439                    }
1440                    .map(|s| s.menu_bounds.children_bounds),
1441                )
1442                .collect::<Vec<_>>();
1443
1444            if menu.is_overlay && menu.close_condition.leave {
1445                for i in (0..state.menu_states.len()).rev() {
1446                    let mb = &state.menu_states[i].menu_bounds;
1447
1448                    if mb.parent_bounds.contains(overlay_cursor)
1449                        || menu.is_overlay && mb.children_bounds.contains(overlay_cursor)
1450                        || mb.offset_bounds.contains(overlay_cursor)
1451                        || (mb.check_bounds.contains(overlay_cursor)
1452                            && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)))
1453                    {
1454                        break;
1455                    }
1456                    prev_bounds.pop();
1457                    state.active_root.pop();
1458                    state.menu_states.pop();
1459                }
1460            } else if menu.is_overlay {
1461                for i in (0..state.menu_states.len()).rev() {
1462                    let mb = &state.menu_states[i].menu_bounds;
1463
1464                    if mb.parent_bounds.contains(overlay_cursor)
1465                        || mb.children_bounds.contains(overlay_cursor)
1466                        || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))
1467                    {
1468                        break;
1469                    }
1470                    prev_bounds.pop();
1471                    state.active_root.pop();
1472                    state.menu_states.pop();
1473                }
1474            }
1475        }
1476
1477        // * update active item
1478        let menu_states_len = state.menu_states.len();
1479
1480        let Some(last_menu_state) = state.menu_states.get_mut(if menu.is_overlay {
1481            menu_states_len.saturating_sub(1)
1482        } else {
1483            menu.depth
1484        }) else {
1485            if menu.is_overlay {
1486                // no menus left
1487                // TODO do we want to avoid this for popups?
1488                // state.active_root.remove(menu.depth);
1489
1490                // keep state.open when the cursor is still inside the menu bar
1491                // this allows the overlay to keep drawing when the cursor is
1492                // moving aroung the menu bar
1493                if !menu.bar_bounds.contains(overlay_cursor) {
1494                    state.open = false;
1495                }
1496            }
1497            shell.capture_event();
1498            return new_menu_root;
1499        };
1500
1501        let last_menu_bounds = &last_menu_state.menu_bounds;
1502        let last_parent_bounds = last_menu_bounds.parent_bounds;
1503        let last_children_bounds = last_menu_bounds.children_bounds;
1504
1505        if (menu.is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor))
1506        // cursor is in the parent part
1507        || menu.is_overlay && !last_children_bounds.contains(overlay_cursor)
1508        // cursor is outside
1509        {
1510
1511            if last_menu_state.index.take().is_some() {
1512                shell.request_redraw();
1513            }
1514            shell.capture_event();
1515            return new_menu_root;
1516        }
1517
1518        // calc new index
1519        let height_diff = (overlay_cursor.y
1520            - (last_children_bounds.y + last_menu_state.scroll_offset))
1521            .clamp(0.0, last_children_bounds.height - 0.001);
1522
1523        let active_root = if menu.is_overlay {
1524            &state.active_root
1525        } else {
1526            &state.active_root[..=menu.depth]
1527        };
1528
1529        if state.pressed {
1530            return new_menu_root;
1531        }
1532        let roots = active_root.iter().skip(1).fold(
1533            &menu.menu_roots[active_root[0]].children,
1534            |mt, next_active_root| &mt[*next_active_root].children,
1535        );
1536        let tree = &mut state.tree.children[active_root[0]].children;
1537
1538        let active_menu: &Vec<MenuTree<Message>> = roots;
1539        let new_index = match menu.item_height {
1540            ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize,
1541            ItemHeight::Static(_) | ItemHeight::Dynamic(_) => {
1542                let max_index = active_menu.len() - 1;
1543                search_bound(
1544                    0,
1545                    0,
1546                    max_index,
1547                    height_diff,
1548                    &last_menu_bounds.child_positions,
1549                    &last_menu_bounds.child_sizes,
1550                )
1551            }
1552        };
1553
1554        let remove = last_menu_state
1555            .index
1556            .as_ref()
1557            .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty());
1558
1559        #[cfg(wayland_platform)]
1560        if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove {
1561            if let Some(id) = state.popup_id.remove(&menu.window_id) {
1562                state.active_root.truncate(menu.depth + 1);
1563                shell.publish((menu.on_surface_action.as_ref().unwrap())({
1564                    crate::surface::action::destroy_popup(id)
1565                }));
1566            }
1567        }
1568        let item = &active_menu[new_index];
1569        // set new index
1570        let old_index = last_menu_state.index.replace(new_index);
1571
1572        if old_index != Some(new_index) {
1573            shell.request_redraw();
1574        }
1575
1576        // get new active item
1577        // * add new menu if the new item is a menu
1578        if !item.children.is_empty() && old_index.is_none_or(|i| i != new_index) {
1579            let item_position = Point::new(
1580                0.0,
1581                last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset,
1582            );
1583            let item_size = last_menu_bounds.child_sizes[new_index];
1584
1585            // overlay space item bounds
1586            let item_bounds = Rectangle::new(item_position, item_size)
1587                + (last_menu_bounds.children_bounds.position() - Point::ORIGIN);
1588
1589            let aod = Aod {
1590                horizontal: true,
1591                vertical: true,
1592                horizontal_overlap: false,
1593                vertical_overlap: true,
1594                horizontal_direction: state.horizontal_direction,
1595                vertical_direction: state.vertical_direction,
1596                horizontal_offset: cross_offset,
1597                vertical_offset: 0.0,
1598            };
1599            let ms = MenuState {
1600                index: None,
1601                scroll_offset: 0.0,
1602                menu_bounds: MenuBounds::new(
1603                    item,
1604                    renderer,
1605                    menu.item_width,
1606                    menu.item_height,
1607                    viewport_size,
1608                    overlay_offset,
1609                    &aod,
1610                    menu.bounds_expand,
1611                    item_bounds,
1612                    tree,
1613                    menu.is_overlay,
1614                ),
1615            };
1616
1617            new_menu_root = Some((new_index, ms.clone()));
1618            if menu.is_overlay {
1619                state.active_root.push(new_index);
1620            } else {
1621                state.menu_states.truncate(menu.depth + 1);
1622            }
1623            state.menu_states.push(ms);
1624        } else if !menu.is_overlay && remove {
1625            state.menu_states.truncate(menu.depth + 1);
1626        }
1627
1628        shell.capture_event();
1629        new_menu_root
1630    })
1631}
1632
1633fn process_scroll_events<Message>(
1634    menu: &mut Menu<'_, Message>,
1635    shell: &mut Shell<'_, Message>,
1636    delta: mouse::ScrollDelta,
1637    overlay_cursor: Point,
1638    viewport_size: Size,
1639    overlay_offset: Vector,
1640) where
1641    Message: Clone,
1642{
1643    use event::Status::{Captured, Ignored};
1644    use mouse::ScrollDelta;
1645
1646    menu.tree.inner.with_data_mut(|state| {
1647        let delta_y = match delta {
1648            ScrollDelta::Lines { y, .. } => y * 60.0,
1649            ScrollDelta::Pixels { y, .. } => y,
1650        };
1651
1652        let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) {
1653            // viewport space children bounds
1654            let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset;
1655
1656            let max_offset = (0.0 - children_bounds.y).max(0.0);
1657            let min_offset =
1658                (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0);
1659            (max_offset, min_offset)
1660        };
1661
1662        // update
1663        if state.menu_states.is_empty() {
1664            return;
1665        } else if state.menu_states.len() == 1 {
1666            let last_ms = &mut state.menu_states[0];
1667
1668            if last_ms.index.is_none() {
1669                return;
1670            }
1671
1672            let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size);
1673            last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset);
1674        } else {
1675            // >= 2
1676            let max_index = state.menu_states.len() - 1;
1677            let last_two = &mut state.menu_states[max_index - 1..=max_index];
1678
1679            if last_two[1].index.is_some() {
1680                // scroll the last one
1681                let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size);
1682                last_two[1].scroll_offset =
1683                    (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset);
1684            } else {
1685                if !last_two[0]
1686                    .menu_bounds
1687                    .children_bounds
1688                    .contains(overlay_cursor)
1689                {
1690                    shell.capture_event();
1691                    return;
1692                }
1693
1694                // scroll the second last one
1695                let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size);
1696                let scroll_offset =
1697                    (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset);
1698                let clamped_delta_y = scroll_offset - last_two[0].scroll_offset;
1699                last_two[0].scroll_offset = scroll_offset;
1700
1701                // update the bounds of the last one
1702                last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y;
1703                last_two[1].menu_bounds.children_bounds.y += clamped_delta_y;
1704                last_two[1].menu_bounds.check_bounds.y += clamped_delta_y;
1705            }
1706        }
1707        shell.capture_event();
1708        shell.request_redraw();
1709    });
1710}
1711
1712#[allow(clippy::pedantic)]
1713/// Returns (children_size, child_positions, child_sizes)
1714fn get_children_layout<Message>(
1715    menu_tree: &MenuTree<Message>,
1716    renderer: &crate::Renderer,
1717    item_width: ItemWidth,
1718    item_height: ItemHeight,
1719    tree: &mut [Tree],
1720) -> (Size, Vec<f32>, Vec<Size>) {
1721    let width = match item_width {
1722        ItemWidth::Uniform(u) => f32::from(u),
1723        ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)),
1724    };
1725
1726    let child_sizes: Vec<Size> = match item_height {
1727        ItemHeight::Uniform(u) => {
1728            let count = menu_tree.children.len();
1729            vec![Size::new(width, f32::from(u)); count]
1730        }
1731        ItemHeight::Static(s) => menu_tree
1732            .children
1733            .iter()
1734            .map(|mt| Size::new(width, f32::from(mt.height.unwrap_or(s))))
1735            .collect(),
1736        ItemHeight::Dynamic(d) => menu_tree
1737            .children
1738            .iter()
1739            .map(|mt| {
1740                mt.item
1741                    .element
1742                    .with_data_mut(|w| match w.as_widget_mut().size().height {
1743                        Length::Fixed(f) => Size::new(width, f),
1744                        Length::Shrink => {
1745                            let l_height = w
1746                                .as_widget_mut()
1747                                .layout(
1748                                    &mut tree[mt.index],
1749                                    renderer,
1750                                    &Limits::new(Size::ZERO, Size::new(width, f32::MAX)),
1751                                )
1752                                .size()
1753                                .height;
1754
1755                            let height = if (f32::MAX - l_height) < 0.001 {
1756                                f32::from(d)
1757                            } else {
1758                                l_height
1759                            };
1760
1761                            Size::new(width, height)
1762                        }
1763                        _ => mt.height.map_or_else(
1764                            || Size::new(width, f32::from(d)),
1765                            |h| Size::new(width, f32::from(h)),
1766                        ),
1767                    })
1768            })
1769            .collect(),
1770    };
1771
1772    let max_index = menu_tree.children.len().saturating_sub(1);
1773    let child_positions: Vec<f32> = std::iter::once(0.0)
1774        .chain(child_sizes[0..max_index].iter().scan(0.0, |acc, x| {
1775            *acc += x.height;
1776            Some(*acc)
1777        }))
1778        .collect();
1779
1780    let height = child_sizes.iter().fold(0.0, |acc, x| acc + x.height);
1781
1782    (Size::new(width, height), child_positions, child_sizes)
1783}
1784
1785fn search_bound(
1786    default: usize,
1787    default_left: usize,
1788    default_right: usize,
1789    bound: f32,
1790    positions: &[f32],
1791    sizes: &[Size],
1792) -> usize {
1793    // binary search
1794    let mut left = default_left;
1795    let mut right = default_right;
1796
1797    let mut index = default;
1798    while left != right {
1799        let m = ((left + right) / 2) + 1;
1800        if positions[m] > bound {
1801            right = m - 1;
1802        } else {
1803            left = m;
1804        }
1805    }
1806    // let height = f32::from(menu_tree.children[left].height.unwrap_or(default_height));
1807    let height = sizes[left].height;
1808    if positions[left] + height > bound {
1809        index = left;
1810    }
1811    index
1812}