cosmic/widget/menu/
menu_inner.rs

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