Skip to main content

gpui_component/menu/
popup_menu.rs

1use crate::ThemeStyled as _;
2use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
3use crate::actions::{SelectLeft, SelectRight};
4use crate::menu::menu_item::MenuItemElement;
5use crate::scroll::ScrollableElement;
6use crate::{ActiveTheme, ElementExt, Icon, IconName, Sizable as _, h_flex, v_flex};
7use crate::{Side, Size, kbd::Kbd};
8use gpui::{
9    Action, Anchor, AnyElement, App, AppContext, Bounds, Context, DismissEvent, Edges, Entity,
10    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
11    ParentElement, Pixels, Render, Role, ScrollHandle, SharedString, StatefulInteractiveElement,
12    Styled, WeakEntity, Window, anchored, deferred, div, prelude::FluentBuilder, px, rems,
13};
14use gpui::{ClickEvent, Half, MouseDownEvent, OwnedMenuItem, Point, Subscription};
15use gpui_base::TestSupportExt as _;
16
17use std::rc::Rc;
18
19const CONTEXT: &str = "PopupMenu";
20
21pub fn init(cx: &mut App) {
22    cx.bind_keys([
23        KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
24        KeyBinding::new("escape", Cancel, Some(CONTEXT)),
25        KeyBinding::new("up", SelectUp, Some(CONTEXT)),
26        KeyBinding::new("down", SelectDown, Some(CONTEXT)),
27        KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
28        KeyBinding::new("right", SelectRight, Some(CONTEXT)),
29    ]);
30}
31
32/// An menu item in a popup menu.
33pub enum PopupMenuItem {
34    /// A menu separator item.
35    Separator,
36    /// A non-interactive label item.
37    Label(SharedString),
38    /// A standard menu item.
39    Item {
40        icon: Option<Icon>,
41        label: SharedString,
42        disabled: bool,
43        checked: bool,
44        is_link: bool,
45        action: Option<Box<dyn Action>>,
46        // For link item
47        handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
48    },
49    /// A menu item with custom element render.
50    ElementItem {
51        icon: Option<Icon>,
52        disabled: bool,
53        checked: bool,
54        action: Option<Box<dyn Action>>,
55        render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
56        handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
57    },
58    /// A submenu item that opens another popup menu.
59    Submenu {
60        icon: Option<Icon>,
61        label: SharedString,
62        disabled: bool,
63        menu: Entity<PopupMenu>,
64    },
65}
66
67impl FluentBuilder for PopupMenuItem {}
68impl PopupMenuItem {
69    /// Create a new menu item with the given label.
70    #[inline]
71    pub fn new(label: impl Into<SharedString>) -> Self {
72        PopupMenuItem::Item {
73            icon: None,
74            label: label.into(),
75            disabled: false,
76            checked: false,
77            action: None,
78            is_link: false,
79            handler: None,
80        }
81    }
82
83    /// Create a new menu item with custom element render.
84    #[inline]
85    pub fn element<F, E>(builder: F) -> Self
86    where
87        F: Fn(&mut Window, &mut App) -> E + 'static,
88        E: IntoElement,
89    {
90        PopupMenuItem::ElementItem {
91            icon: None,
92            disabled: false,
93            checked: false,
94            action: None,
95            render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
96            handler: None,
97        }
98    }
99
100    /// Create a new submenu item that opens another popup menu.
101    #[inline]
102    pub fn submenu(label: impl Into<SharedString>, menu: Entity<PopupMenu>) -> Self {
103        PopupMenuItem::Submenu {
104            icon: None,
105            label: label.into(),
106            disabled: false,
107            menu,
108        }
109    }
110
111    /// Create a separator menu item.
112    #[inline]
113    pub fn separator() -> Self {
114        PopupMenuItem::Separator
115    }
116
117    /// Creates a label menu item.
118    #[inline]
119    pub fn label(label: impl Into<SharedString>) -> Self {
120        PopupMenuItem::Label(label.into())
121    }
122
123    /// Set the icon for the menu item.
124    ///
125    /// Only works for [`PopupMenuItem::Item`], [`PopupMenuItem::ElementItem`] and [`PopupMenuItem::Submenu`].
126    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
127        match &mut self {
128            PopupMenuItem::Item { icon: i, .. } => {
129                *i = Some(icon.into());
130            }
131            PopupMenuItem::ElementItem { icon: i, .. } => {
132                *i = Some(icon.into());
133            }
134            PopupMenuItem::Submenu { icon: i, .. } => {
135                *i = Some(icon.into());
136            }
137            _ => {}
138        }
139        self
140    }
141
142    /// Set the action for the menu item.
143    ///
144    /// Only works for [`PopupMenuItem::Item`] and [`PopupMenuItem::ElementItem`].
145    pub fn action(mut self, action: Box<dyn Action>) -> Self {
146        match &mut self {
147            PopupMenuItem::Item { action: a, .. } => {
148                *a = Some(action);
149            }
150            PopupMenuItem::ElementItem { action: a, .. } => {
151                *a = Some(action);
152            }
153            _ => {}
154        }
155        self
156    }
157
158    /// Set the disabled state for the menu item.
159    ///
160    /// Only works for [`PopupMenuItem::Item`], [`PopupMenuItem::ElementItem`] and [`PopupMenuItem::Submenu`].
161    pub fn disabled(mut self, disabled: bool) -> Self {
162        match &mut self {
163            PopupMenuItem::Item { disabled: d, .. } => {
164                *d = disabled;
165            }
166            PopupMenuItem::ElementItem { disabled: d, .. } => {
167                *d = disabled;
168            }
169            PopupMenuItem::Submenu { disabled: d, .. } => {
170                *d = disabled;
171            }
172            _ => {}
173        }
174        self
175    }
176
177    /// Set checked state for the menu item.
178    ///
179    /// NOTE: If `check_side` is [`Side::Left`], the icon will replace with a check icon.
180    pub fn checked(mut self, checked: bool) -> Self {
181        match &mut self {
182            PopupMenuItem::Item { checked: c, .. } => {
183                *c = checked;
184            }
185            PopupMenuItem::ElementItem { checked: c, .. } => {
186                *c = checked;
187            }
188            _ => {}
189        }
190        self
191    }
192
193    /// Add a click handler for the menu item.
194    ///
195    /// Only works for [`PopupMenuItem::Item`] and [`PopupMenuItem::ElementItem`].
196    pub fn on_click<F>(mut self, handler: F) -> Self
197    where
198        F: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
199    {
200        match &mut self {
201            PopupMenuItem::Item { handler: h, .. } => {
202                *h = Some(Rc::new(handler));
203            }
204            PopupMenuItem::ElementItem { handler: h, .. } => {
205                *h = Some(Rc::new(handler));
206            }
207            _ => {}
208        }
209        self
210    }
211
212    /// Create a link menu item.
213    #[inline]
214    pub fn link(label: impl Into<SharedString>, href: impl Into<String>) -> Self {
215        let href = href.into();
216        PopupMenuItem::Item {
217            icon: None,
218            label: label.into(),
219            disabled: false,
220            checked: false,
221            action: None,
222            is_link: true,
223            handler: Some(Rc::new(move |_, _, cx| cx.open_url(&href))),
224        }
225    }
226
227    #[inline]
228    fn is_clickable(&self) -> bool {
229        !matches!(self, PopupMenuItem::Separator)
230            && matches!(
231                self,
232                PopupMenuItem::Item {
233                    disabled: false,
234                    ..
235                } | PopupMenuItem::ElementItem {
236                    disabled: false,
237                    ..
238                } | PopupMenuItem::Submenu {
239                    disabled: false,
240                    ..
241                }
242            )
243    }
244
245    #[inline]
246    fn is_separator(&self) -> bool {
247        matches!(self, PopupMenuItem::Separator)
248    }
249
250    fn has_left_icon(&self, check_side: Side) -> bool {
251        match self {
252            PopupMenuItem::Item { icon, checked, .. } => {
253                icon.is_some() || (check_side.is_left() && *checked)
254            }
255            PopupMenuItem::ElementItem { icon, checked, .. } => {
256                icon.is_some() || (check_side.is_left() && *checked)
257            }
258            PopupMenuItem::Submenu { icon, .. } => icon.is_some(),
259            _ => false,
260        }
261    }
262
263    #[inline]
264    fn is_checked(&self) -> bool {
265        match self {
266            PopupMenuItem::Item { checked, .. } => *checked,
267            PopupMenuItem::ElementItem { checked, .. } => *checked,
268            _ => false,
269        }
270    }
271
272    fn a11y_label(&self) -> Option<SharedString> {
273        match self {
274            PopupMenuItem::Item { label, .. }
275            | PopupMenuItem::Label(label)
276            | PopupMenuItem::Submenu { label, .. } => Some(label.clone()),
277            PopupMenuItem::Separator | PopupMenuItem::ElementItem { .. } => None,
278        }
279    }
280}
281
282pub struct PopupMenu {
283    pub(crate) focus_handle: FocusHandle,
284    pub(crate) menu_items: Vec<PopupMenuItem>,
285    /// The focus handle of Entity to handle actions.
286    pub(crate) action_context: Option<FocusHandle>,
287    /// The focus to restore on dismiss. Unlike `action_context`, this does not
288    /// change where actions are dispatched: they still bubble from the menu's
289    /// own focus path (through the trigger element's ancestors).
290    pub(crate) previous_focus_handle: Option<FocusHandle>,
291    /// A focus handle on the trigger's dispatch path, so shortcut hints can
292    /// resolve against the key contexts the menu's actions bubble through on
293    /// the very frame the menu opens. GPUI looks a handle up in the previously
294    /// rendered frame, and the trigger was in it when the menu was not yet.
295    pub(crate) trigger_focus_handle: Option<FocusHandle>,
296    selected_index: Option<usize>,
297    min_width: Option<Pixels>,
298    max_width: Option<Pixels>,
299    max_height: Option<Pixels>,
300    bounds: Bounds<Pixels>,
301    size: Size,
302    check_side: Side,
303
304    /// The parent menu of this menu, if this is a submenu
305    parent_menu: Option<WeakEntity<Self>>,
306    scrollable: bool,
307    external_link_icon: bool,
308    scroll_handle: ScrollHandle,
309    // This will update on render
310    submenu_anchor: (Anchor, Pixels),
311
312    /// Paint priority for this menu layer. The top-level menu starts at 1 and
313    /// each nested submenu increments it, so deeper levels are always drawn on
314    /// top of shallower ones. This fixes background content (e.g. the
315    /// underlying list) bleeding through multi-level submenus, which happens
316    /// when nested `anchored` popovers share the same paint order.
317    ///
318    /// The top-level menu relies on its container (e.g. `Popover`,
319    /// `ContextMenu`) to `deferred`-draw it, and each submenu is deferred once
320    /// in `render_item` with `priority + 1`. Keeping a single deferred layer
321    /// per level matters because GPUI caps nested deferred depth (see
322    /// `prepaint_deferred_draws`).
323    ///
324    /// Deferring is also what lets a submenu open from a `scrollable` menu:
325    /// GPUI paints a deferred draw at the window level with no inherited
326    /// content mask, so the items container's `overflow_y_scroll` clip never
327    /// reaches it, while the scroll offset is still baked into its anchor.
328    priority: usize,
329
330    _subscriptions: Vec<Subscription>,
331}
332
333impl PopupMenu {
334    pub(crate) fn new(cx: &mut App) -> Self {
335        Self {
336            focus_handle: cx.focus_handle(),
337            action_context: None,
338            previous_focus_handle: None,
339            trigger_focus_handle: None,
340            parent_menu: None,
341            menu_items: Vec::new(),
342            selected_index: None,
343            min_width: None,
344            max_width: None,
345            max_height: None,
346            check_side: Side::Left,
347            bounds: Bounds::default(),
348            scrollable: false,
349            scroll_handle: ScrollHandle::default(),
350            external_link_icon: true,
351            size: Size::default(),
352            submenu_anchor: (Anchor::TopLeft, Pixels::ZERO),
353            priority: gpui_base::POPUP_PRIORITY,
354            _subscriptions: vec![],
355        }
356    }
357
358    pub fn build(
359        window: &mut Window,
360        cx: &mut App,
361        f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
362    ) -> Entity<Self> {
363        cx.new(|cx| f(Self::new(cx), window, cx))
364    }
365
366    /// Set the focus handle of Entity to handle actions.
367    ///
368    /// When the menu is dismissed or before an action is triggered, the focus will be returned to this handle.
369    ///
370    /// Then the action will be dispatched to this handle.
371    /// Submenus without their own action context use the nearest parent's
372    /// explicit context for actions and shortcut hints.
373    pub fn action_context(mut self, handle: FocusHandle) -> Self {
374        self.action_context = Some(handle);
375        self
376    }
377
378    pub(crate) fn set_action_context(
379        &mut self,
380        action_context: Option<FocusHandle>,
381        cx: &mut Context<Self>,
382    ) {
383        self.action_context = action_context.clone();
384
385        for item in &self.menu_items {
386            if let PopupMenuItem::Submenu { menu, .. } = item {
387                menu.update(cx, |menu, cx| {
388                    menu.set_action_context(action_context.clone(), cx);
389                });
390            }
391        }
392    }
393
394    /// Set the focus to restore when the menu is dismissed, without changing
395    /// where actions are dispatched.
396    pub(crate) fn set_previous_focus(
397        &mut self,
398        handle: Option<FocusHandle>,
399        cx: &mut Context<Self>,
400    ) {
401        self.previous_focus_handle = handle.clone();
402
403        for item in &self.menu_items {
404            if let PopupMenuItem::Submenu { menu, .. } = item {
405                menu.update(cx, |menu, cx| {
406                    menu.set_previous_focus(handle.clone(), cx);
407                });
408            }
409        }
410    }
411
412    /// Set the focus handle on the trigger's dispatch path that shortcut hints
413    /// resolve against, without changing focus or where actions are dispatched.
414    pub(crate) fn set_trigger_focus(
415        &mut self,
416        handle: Option<FocusHandle>,
417        cx: &mut Context<Self>,
418    ) {
419        self.trigger_focus_handle = handle.clone();
420
421        for item in &self.menu_items {
422            if let PopupMenuItem::Submenu { menu, .. } = item {
423                menu.update(cx, |menu, cx| {
424                    menu.set_trigger_focus(handle.clone(), cx);
425                });
426            }
427        }
428    }
429
430    /// Set min width of the popup menu, default is 120px
431    pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
432        self.min_width = Some(width.into());
433        self
434    }
435
436    /// Set max width of the popup menu, default is 500px
437    pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
438        self.max_width = Some(width.into());
439        self
440    }
441
442    /// Set max height of the popup menu, default is half of the window height
443    pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
444        self.max_height = Some(height.into());
445        self
446    }
447
448    /// Set the menu to be scrollable to show vertical scrollbar.
449    pub fn scrollable(mut self, scrollable: bool) -> Self {
450        self.scrollable = scrollable;
451        self
452    }
453
454    /// Set the side to show check icon, default is `Side::Left`.
455    pub fn check_side(mut self, side: Side) -> Self {
456        self.check_side = side;
457        self
458    }
459
460    /// Set the menu to show external link icon, default is true.
461    pub fn external_link_icon(mut self, visible: bool) -> Self {
462        self.external_link_icon = visible;
463        self
464    }
465
466    /// Add Menu Item
467    pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
468        self.menu_with_disabled(label, action, false)
469    }
470
471    /// Add Menu Item with enable state
472    pub fn menu_with_enable(
473        mut self,
474        label: impl Into<SharedString>,
475        action: Box<dyn Action>,
476        enable: bool,
477    ) -> Self {
478        self.add_menu_item(label, None, action, !enable, false);
479        self
480    }
481
482    /// Add Menu Item with disabled state
483    pub fn menu_with_disabled(
484        mut self,
485        label: impl Into<SharedString>,
486        action: Box<dyn Action>,
487        disabled: bool,
488    ) -> Self {
489        self.add_menu_item(label, None, action, disabled, false);
490        self
491    }
492
493    /// Add label
494    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
495        self.menu_items.push(PopupMenuItem::label(label.into()));
496        self
497    }
498
499    /// Add Menu to open link
500    pub fn link(self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
501        self.link_with_disabled(label, href, false)
502    }
503
504    /// Add Menu to open link with disabled state
505    pub fn link_with_disabled(
506        mut self,
507        label: impl Into<SharedString>,
508        href: impl Into<String>,
509        disabled: bool,
510    ) -> Self {
511        let href = href.into();
512        self.menu_items
513            .push(PopupMenuItem::link(label, href).disabled(disabled));
514        self
515    }
516
517    /// Add Menu to open link
518    pub fn link_with_icon(
519        self,
520        label: impl Into<SharedString>,
521        icon: impl Into<Icon>,
522        href: impl Into<String>,
523    ) -> Self {
524        self.link_with_icon_and_disabled(label, icon, href, false)
525    }
526
527    /// Add Menu to open link with icon and disabled state
528    fn link_with_icon_and_disabled(
529        mut self,
530        label: impl Into<SharedString>,
531        icon: impl Into<Icon>,
532        href: impl Into<String>,
533        disabled: bool,
534    ) -> Self {
535        let href = href.into();
536        self.menu_items.push(
537            PopupMenuItem::link(label, href)
538                .icon(icon)
539                .disabled(disabled),
540        );
541        self
542    }
543
544    /// Add Menu Item with Icon.
545    pub fn menu_with_icon(
546        self,
547        label: impl Into<SharedString>,
548        icon: impl Into<Icon>,
549        action: Box<dyn Action>,
550    ) -> Self {
551        self.menu_with_icon_and_disabled(label, icon, action, false)
552    }
553
554    /// Add Menu Item with Icon and disabled state
555    pub fn menu_with_icon_and_disabled(
556        mut self,
557        label: impl Into<SharedString>,
558        icon: impl Into<Icon>,
559        action: Box<dyn Action>,
560        disabled: bool,
561    ) -> Self {
562        self.add_menu_item(label, Some(icon.into()), action, disabled, false);
563        self
564    }
565
566    /// Add Menu Item with check icon
567    pub fn menu_with_check(
568        self,
569        label: impl Into<SharedString>,
570        checked: bool,
571        action: Box<dyn Action>,
572    ) -> Self {
573        self.menu_with_check_and_disabled(label, checked, action, false)
574    }
575
576    /// Add Menu Item with check icon and disabled state
577    pub fn menu_with_check_and_disabled(
578        mut self,
579        label: impl Into<SharedString>,
580        checked: bool,
581        action: Box<dyn Action>,
582        disabled: bool,
583    ) -> Self {
584        self.add_menu_item(label, None, action, disabled, checked);
585        self
586    }
587
588    /// Add Menu Item with custom element render.
589    pub fn menu_element<F, E>(self, action: Box<dyn Action>, builder: F) -> Self
590    where
591        F: Fn(&mut Window, &mut App) -> E + 'static,
592        E: IntoElement,
593    {
594        self.menu_element_with_check(false, action, builder)
595    }
596
597    /// Add Menu Item with custom element render with disabled state.
598    pub fn menu_element_with_disabled<F, E>(
599        self,
600        action: Box<dyn Action>,
601        disabled: bool,
602        builder: F,
603    ) -> Self
604    where
605        F: Fn(&mut Window, &mut App) -> E + 'static,
606        E: IntoElement,
607    {
608        self.menu_element_with_check_and_disabled(false, action, disabled, builder)
609    }
610
611    /// Add Menu Item with custom element render with icon.
612    pub fn menu_element_with_icon<F, E>(
613        self,
614        icon: impl Into<Icon>,
615        action: Box<dyn Action>,
616        builder: F,
617    ) -> Self
618    where
619        F: Fn(&mut Window, &mut App) -> E + 'static,
620        E: IntoElement,
621    {
622        self.menu_element_with_icon_and_disabled(icon, action, false, builder)
623    }
624
625    /// Add Menu Item with custom element render with check state
626    pub fn menu_element_with_check<F, E>(
627        self,
628        checked: bool,
629        action: Box<dyn Action>,
630        builder: F,
631    ) -> Self
632    where
633        F: Fn(&mut Window, &mut App) -> E + 'static,
634        E: IntoElement,
635    {
636        self.menu_element_with_check_and_disabled(checked, action, false, builder)
637    }
638
639    /// Add Menu Item with custom element render with icon and disabled state
640    fn menu_element_with_icon_and_disabled<F, E>(
641        mut self,
642        icon: impl Into<Icon>,
643        action: Box<dyn Action>,
644        disabled: bool,
645        builder: F,
646    ) -> Self
647    where
648        F: Fn(&mut Window, &mut App) -> E + 'static,
649        E: IntoElement,
650    {
651        self.menu_items.push(
652            PopupMenuItem::element(builder)
653                .action(action)
654                .icon(icon)
655                .disabled(disabled),
656        );
657        self
658    }
659
660    /// Add Menu Item with custom element render with check state and disabled state
661    fn menu_element_with_check_and_disabled<F, E>(
662        mut self,
663        checked: bool,
664        action: Box<dyn Action>,
665        disabled: bool,
666        builder: F,
667    ) -> Self
668    where
669        F: Fn(&mut Window, &mut App) -> E + 'static,
670        E: IntoElement,
671    {
672        self.menu_items.push(
673            PopupMenuItem::element(builder)
674                .action(action)
675                .checked(checked)
676                .disabled(disabled),
677        );
678        self
679    }
680
681    /// Add a separator Menu Item
682    pub fn separator(mut self) -> Self {
683        if self.menu_items.is_empty() {
684            return self;
685        }
686
687        if let Some(PopupMenuItem::Separator) = self.menu_items.last() {
688            return self;
689        }
690
691        self.menu_items.push(PopupMenuItem::separator());
692        self
693    }
694
695    /// Add a Submenu
696    pub fn submenu(
697        self,
698        label: impl Into<SharedString>,
699        window: &mut Window,
700        cx: &mut Context<Self>,
701        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
702    ) -> Self {
703        self.submenu_with_icon(None, label, window, cx, f)
704    }
705
706    /// Add a Submenu item with icon
707    pub fn submenu_with_icon(
708        mut self,
709        icon: Option<Icon>,
710        label: impl Into<SharedString>,
711        window: &mut Window,
712        cx: &mut Context<Self>,
713        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
714    ) -> Self {
715        let submenu = PopupMenu::build(window, cx, f);
716        let parent_menu = cx.entity().downgrade();
717        let parent_priority = self.priority;
718        submenu.update(cx, |view, _| {
719            view.parent_menu = Some(parent_menu);
720            view.priority = parent_priority + 1;
721        });
722
723        self.menu_items.push(
724            PopupMenuItem::submenu(label, submenu).when_some(icon, |this, icon| this.icon(icon)),
725        );
726        self
727    }
728
729    /// Add menu item.
730    pub fn item(mut self, item: impl Into<PopupMenuItem>) -> Self {
731        let item: PopupMenuItem = item.into();
732        self.menu_items.push(item);
733        self
734    }
735
736    /// Replace all menu items by re-running a builder on this menu, keeping its
737    /// identity (focus, parent menu, layer priority).
738    ///
739    /// For menus whose content arrives asynchronously after the menu is shown,
740    /// e.g. swapping a "loading…" placeholder for the loaded items:
741    ///
742    /// ```ignore
743    /// cx.spawn_in(window, async move |menu, cx| {
744    ///     let items = fetch_items().await;
745    ///     _ = menu.update_in(cx, |menu, window, cx| {
746    ///         menu.rebuild(window, cx, |menu, _, _| {
747    ///             items.into_iter().fold(menu, |menu, item| {
748    ///                 menu.menu(item.label, Box::new(item.action))
749    ///             })
750    ///         });
751    ///     });
752    /// })
753    /// .detach();
754    /// ```
755    pub fn rebuild(
756        &mut self,
757        window: &mut Window,
758        cx: &mut Context<Self>,
759        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
760    ) {
761        let mut menu = std::mem::replace(self, Self::new(cx));
762        menu.menu_items.clear();
763        menu.selected_index = None;
764        *self = f(menu, window, cx);
765        cx.notify();
766    }
767
768    fn add_menu_item(
769        &mut self,
770        label: impl Into<SharedString>,
771        icon: Option<Icon>,
772        action: Box<dyn Action>,
773        disabled: bool,
774        checked: bool,
775    ) -> &mut Self {
776        self.menu_items.push(
777            PopupMenuItem::new(label)
778                .when_some(icon, |item, icon| item.icon(icon))
779                .disabled(disabled)
780                .checked(checked)
781                .action(action),
782        );
783        self
784    }
785
786    pub(super) fn with_menu_items<I>(
787        mut self,
788        items: impl IntoIterator<Item = I>,
789        window: &mut Window,
790        cx: &mut Context<Self>,
791    ) -> Self
792    where
793        I: Into<OwnedMenuItem>,
794    {
795        for item in items {
796            match item.into() {
797                OwnedMenuItem::Action {
798                    name,
799                    action,
800                    checked,
801                    disabled,
802                    ..
803                } => {
804                    self = self.menu_with_check_and_disabled(
805                        name,
806                        checked,
807                        action.boxed_clone(),
808                        disabled,
809                    )
810                }
811                OwnedMenuItem::Separator => {
812                    self = self.separator();
813                }
814                OwnedMenuItem::Submenu(submenu) => {
815                    self = self.submenu(submenu.name, window, cx, move |menu, window, cx| {
816                        menu.with_menu_items(submenu.items.clone(), window, cx)
817                    })
818                }
819                OwnedMenuItem::SystemMenu(_) => {}
820            }
821        }
822
823        if self.menu_items.len() > 20 {
824            self.scrollable = true;
825        }
826
827        self
828    }
829
830    pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
831        if let Some(ix) = self.selected_index {
832            if let Some(item) = self.menu_items.get(ix) {
833                return match item {
834                    PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
835                    _ => None,
836                };
837            }
838        }
839
840        None
841    }
842
843    pub fn is_empty(&self) -> bool {
844        self.menu_items.is_empty()
845    }
846
847    fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
848        self.menu_items
849            .iter()
850            .enumerate()
851            .filter(|(_, item)| item.is_clickable())
852    }
853
854    fn on_click(
855        &mut self,
856        ix: usize,
857        event: &ClickEvent,
858        window: &mut Window,
859        cx: &mut Context<Self>,
860    ) {
861        cx.stop_propagation();
862        window.prevent_default();
863        self.selected_index = Some(ix);
864        self.activate(event, window, cx);
865    }
866
867    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
868        self.activate(&ClickEvent::default(), window, cx);
869    }
870
871    fn activate(&mut self, event: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
872        match self.selected_index {
873            Some(index) => {
874                let item = self.menu_items.get(index);
875                match item {
876                    Some(PopupMenuItem::Item {
877                        handler, action, ..
878                    }) => {
879                        if let Some(handler) = handler {
880                            handler(event, window, cx);
881                        } else if let Some(action) = action.as_ref() {
882                            self.dispatch_confirm_action(action, window, cx);
883                        }
884
885                        self.dismiss(&Cancel, window, cx)
886                    }
887                    Some(PopupMenuItem::ElementItem {
888                        handler, action, ..
889                    }) => {
890                        if let Some(handler) = handler {
891                            handler(event, window, cx);
892                        } else if let Some(action) = action.as_ref() {
893                            self.dispatch_confirm_action(action, window, cx);
894                        }
895                        self.dismiss(&Cancel, window, cx)
896                    }
897                    _ => {}
898                }
899            }
900            _ => {}
901        }
902    }
903
904    fn dispatch_confirm_action(
905        &self,
906        action: &Box<dyn Action>,
907        window: &mut Window,
908        cx: &mut Context<Self>,
909    ) {
910        if let Some(context) = self.resolved_action_context(cx) {
911            context.focus(window, cx);
912        }
913
914        window.dispatch_action(action.boxed_clone(), cx);
915    }
916
917    fn resolved_action_context(&self, cx: &App) -> Option<FocusHandle> {
918        self.action_context.clone().or_else(|| {
919            let parent = self.parent_menu.as_ref()?.upgrade()?;
920            parent.read(cx).resolved_action_context(cx)
921        })
922    }
923
924    fn set_selected_index(&mut self, ix: usize, cx: &mut Context<Self>) {
925        if self.selected_index != Some(ix) {
926            self.selected_index = Some(ix);
927            self.scroll_handle.scroll_to_item(ix);
928            cx.notify();
929        }
930    }
931
932    fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
933        cx.stop_propagation();
934        let ix = self.selected_index.unwrap_or(0);
935
936        if let Some((prev_ix, _)) = self
937            .menu_items
938            .iter()
939            .enumerate()
940            .rev()
941            .find(|(i, item)| *i < ix && item.is_clickable())
942        {
943            self.set_selected_index(prev_ix, cx);
944            return;
945        }
946
947        let last_clickable_ix = self.clickable_menu_items().last().map(|(ix, _)| ix);
948        self.set_selected_index(last_clickable_ix.unwrap_or(0), cx);
949    }
950
951    fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
952        cx.stop_propagation();
953        let Some(ix) = self.selected_index else {
954            self.set_selected_index(0, cx);
955            return;
956        };
957
958        if let Some((next_ix, _)) = self
959            .menu_items
960            .iter()
961            .enumerate()
962            .find(|(i, item)| *i > ix && item.is_clickable())
963        {
964            self.set_selected_index(next_ix, cx);
965            return;
966        }
967
968        self.set_selected_index(0, cx);
969    }
970
971    fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
972        let handled = if matches!(self.submenu_anchor.0, Anchor::TopLeft | Anchor::BottomLeft) {
973            self._unselect_submenu(window, cx)
974        } else {
975            self._select_submenu(window, cx)
976        };
977
978        if self.parent_side(cx).is_left() {
979            self._focus_parent_menu(window, cx);
980        }
981
982        if handled {
983            return;
984        }
985
986        // For parent AppMenuBar to handle.
987        if self.parent_menu.is_none() {
988            cx.propagate();
989        }
990    }
991
992    fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
993        let handled = if matches!(self.submenu_anchor.0, Anchor::TopLeft | Anchor::BottomLeft) {
994            self._select_submenu(window, cx)
995        } else {
996            self._unselect_submenu(window, cx)
997        };
998
999        if self.parent_side(cx).is_right() {
1000            self._focus_parent_menu(window, cx);
1001        }
1002
1003        if handled {
1004            return;
1005        }
1006
1007        // For parent AppMenuBar to handle.
1008        if self.parent_menu.is_none() {
1009            cx.propagate();
1010        }
1011    }
1012
1013    fn _select_submenu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
1014        if let Some(active_submenu) = self.active_submenu() {
1015            // Focus the submenu, so that can be handle the action.
1016            active_submenu.update(cx, |view, cx| {
1017                view.set_selected_index(0, cx);
1018                view.focus_handle.focus(window, cx);
1019            });
1020            cx.notify();
1021            return true;
1022        }
1023
1024        return false;
1025    }
1026
1027    fn _unselect_submenu(&mut self, _: &mut Window, cx: &mut Context<Self>) -> bool {
1028        if let Some(active_submenu) = self.active_submenu() {
1029            active_submenu.update(cx, |view, cx| {
1030                view.selected_index = None;
1031                cx.notify();
1032            });
1033            return true;
1034        }
1035
1036        return false;
1037    }
1038
1039    fn _focus_parent_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1040        let Some(parent) = self.parent_menu.as_ref() else {
1041            return;
1042        };
1043        let Some(parent) = parent.upgrade() else {
1044            return;
1045        };
1046
1047        self.selected_index = None;
1048        parent.update(cx, |view, cx| {
1049            view.focus_handle.focus(window, cx);
1050            cx.notify();
1051        });
1052    }
1053
1054    fn parent_side(&self, cx: &App) -> Side {
1055        let Some(parent) = self.parent_menu.as_ref() else {
1056            return Side::Left;
1057        };
1058
1059        let Some(parent) = parent.upgrade() else {
1060            return Side::Left;
1061        };
1062
1063        match parent.read(cx).submenu_anchor.0 {
1064            Anchor::TopLeft | Anchor::BottomLeft => Side::Left,
1065            Anchor::TopRight | Anchor::BottomRight => Side::Right,
1066            // Center anchors are not used for submenu positioning, but we must cover them.
1067            _ => Side::Left,
1068        }
1069    }
1070
1071    /// Dismiss the menu and the entire parent chain.
1072    ///
1073    /// The submenu is closed together with its parent, same as macOS menus.
1074    fn dismiss(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
1075        self.selected_index = None;
1076        cx.emit(DismissEvent);
1077
1078        // Focus back to the previous focused handle, unless the item's click
1079        // handler has already moved focus elsewhere (e.g. opened a dialog and
1080        // focused its input) -- stealing focus back would break that.
1081        let focus_moved_away =
1082            window.focused(cx).is_some() && !self.focus_handle.contains_focused(window, cx);
1083        if !focus_moved_away {
1084            if let Some(handle) = self
1085                .previous_focus_handle
1086                .as_ref()
1087                .or(self.action_context.as_ref())
1088            {
1089                window.focus(handle, cx);
1090            }
1091        }
1092
1093        let Some(parent_menu) = self.parent_menu.clone() else {
1094            return;
1095        };
1096
1097        // Dismiss parent menu, when this menu is dismissed
1098        _ = parent_menu.update(cx, |view, cx| {
1099            view.dismiss(&Cancel, window, cx);
1100        });
1101    }
1102
1103    fn handle_dismiss(
1104        &mut self,
1105        position: &Point<Pixels>,
1106        window: &mut Window,
1107        cx: &mut Context<Self>,
1108    ) {
1109        // Do not dismiss, if click inside the parent menu
1110        if let Some(parent) = self.parent_menu.as_ref() {
1111            if let Some(parent) = parent.upgrade() {
1112                if parent.read(cx).bounds.contains(position) {
1113                    return;
1114                }
1115            }
1116        }
1117
1118        // Do not dismiss, if there have an active submenu, the click may be
1119        // inside the submenu, let the submenu to handle it.
1120        //
1121        // Otherwise the submenu will be dismissed before its item's `on_click`.
1122        if self.active_submenu().is_some() {
1123            return;
1124        }
1125
1126        self.dismiss(&Cancel, window, cx);
1127    }
1128
1129    fn on_mouse_down_out(
1130        &mut self,
1131        e: &MouseDownEvent,
1132        window: &mut Window,
1133        cx: &mut Context<Self>,
1134    ) {
1135        self.handle_dismiss(&e.position, window, cx);
1136    }
1137
1138    fn render_key_binding(
1139        &self,
1140        action: Option<Box<dyn Action>>,
1141        window: &mut Window,
1142        cx: &mut Context<Self>,
1143    ) -> Option<Kbd> {
1144        let action = action?;
1145
1146        // Each handle names a dispatch path GPUI can resolve in the previously
1147        // rendered frame: the explicit action target, the trigger the menu
1148        // opened from (already rendered when the menu first draws), the focus
1149        // the menu interrupted, then the menu's own path once it has been
1150        // rendered. A binding registered without a key context applies on
1151        // every path, so it is the last resort rather than the window's
1152        // leftover context stack, which only holds the path of whatever
1153        // element happened to paint last.
1154        let action_context = self.resolved_action_context(cx);
1155        [
1156            action_context.as_ref(),
1157            self.trigger_focus_handle.as_ref(),
1158            self.previous_focus_handle.as_ref(),
1159            Some(&self.focus_handle),
1160        ]
1161        .into_iter()
1162        .flatten()
1163        .find_map(|handle| Kbd::binding_for_action_in(action.as_ref(), handle, window))
1164        .or_else(|| Kbd::global_binding_for_action(action.as_ref(), window))
1165        .map(|this| {
1166            this.p_0()
1167                .flex_nowrap()
1168                .border_0()
1169                .bg(gpui::transparent_white())
1170        })
1171    }
1172
1173    fn render_icon(
1174        has_icon: bool,
1175        checked: bool,
1176        icon: Option<Icon>,
1177        _: &mut Window,
1178        _: &mut Context<Self>,
1179    ) -> Option<impl IntoElement> {
1180        if !has_icon {
1181            return None;
1182        }
1183
1184        let icon = if let Some(icon) = icon {
1185            icon.clone()
1186        } else if checked {
1187            Icon::new(IconName::Check)
1188        } else {
1189            Icon::empty()
1190        };
1191
1192        Some(icon.xsmall())
1193    }
1194
1195    #[inline]
1196    fn max_width(&self) -> Pixels {
1197        self.max_width.unwrap_or(px(500.))
1198    }
1199
1200    /// Calculate the anchor corner and left offset for child submenu
1201    fn update_submenu_menu_anchor(&mut self, window: &Window) {
1202        let bounds = self.bounds;
1203        let max_width = self.max_width();
1204        let (anchor, left) = if max_width + bounds.origin.x > window.bounds().size.width {
1205            (Anchor::TopRight, -px(16.))
1206        } else {
1207            (Anchor::TopLeft, bounds.size.width - px(8.))
1208        };
1209
1210        let is_bottom_pos = bounds.origin.y + bounds.size.height > window.bounds().size.height;
1211        self.submenu_anchor = if is_bottom_pos {
1212            (anchor.other_side_along(gpui::Axis::Vertical), left)
1213        } else {
1214            (anchor, left)
1215        };
1216    }
1217
1218    fn render_item(
1219        &self,
1220        ix: usize,
1221        item: &PopupMenuItem,
1222        options: RenderOptions,
1223        window: &mut Window,
1224        cx: &mut Context<Self>,
1225    ) -> MenuItemElement {
1226        let has_left_icon = options.has_left_icon;
1227        let is_left_check = options.check_side.is_left() && item.is_checked();
1228        let right_check_icon = if options.check_side.is_right() && item.is_checked() {
1229            Some(Icon::new(IconName::Check).xsmall())
1230        } else {
1231            None
1232        };
1233
1234        let selected = self.selected_index == Some(ix);
1235        const EDGE_PADDING: Pixels = px(4.);
1236        const INNER_PADDING: Pixels = px(8.);
1237
1238        let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
1239
1240        let (item_height, radius) = match self.size {
1241            Size::Small => (px(20.), options.radius.half()),
1242            _ => (px(26.), options.radius),
1243        };
1244
1245        let this = MenuItemElement::new(ix)
1246            .relative()
1247            .text_sm()
1248            .py_0()
1249            .px(INNER_PADDING)
1250            .rounded(radius)
1251            .items_center()
1252            .selected(selected)
1253            .on_hover(cx.listener(move |this, hovered, window, cx| {
1254                if *hovered {
1255                    // Pointing elsewhere closes the open submenu; take back
1256                    // the focus the keyboard may have moved into it.
1257                    if this.selected_index != Some(ix)
1258                        && this.active_submenu().is_some_and(|submenu| {
1259                            submenu.focus_handle(cx).contains_focused(window, cx)
1260                        })
1261                    {
1262                        this.focus_handle.focus(window, cx);
1263                    }
1264                    this.selected_index = Some(ix);
1265                } else if !is_submenu
1266                    && this.selected_index == Some(ix)
1267                    // A key press ends hover under a still pointer; keep the
1268                    // highlight so the next arrow key moves on from this item.
1269                    && !window.last_input_was_keyboard()
1270                {
1271                    // TODO: Better handle the submenu unselection when hover out
1272                    this.selected_index = None;
1273                }
1274
1275                cx.notify();
1276            }))
1277            .when_some(item.a11y_label(), |this, label| this.aria_label(label));
1278
1279        match item {
1280            PopupMenuItem::Separator => this
1281                .h_auto()
1282                .p_0()
1283                .my_0p5()
1284                .mx_neg_1()
1285                .border_b(px(2.))
1286                .border_color(cx.theme().border)
1287                .disabled(true),
1288            PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
1289                h_flex()
1290                    .cursor_default()
1291                    .items_center()
1292                    .gap_x_1()
1293                    .children(Self::render_icon(has_left_icon, false, None, window, cx))
1294                    .child(div().flex_1().child(label.clone())),
1295            ),
1296            PopupMenuItem::ElementItem {
1297                render,
1298                icon,
1299                disabled,
1300                ..
1301            } => this
1302                .when(!disabled, |this| {
1303                    this.on_click(cx.listener(move |this, event, window, cx| {
1304                        this.on_click(ix, event, window, cx)
1305                    }))
1306                })
1307                .disabled(*disabled)
1308                .child(
1309                    h_flex()
1310                        .flex_1()
1311                        .min_h(item_height)
1312                        .items_center()
1313                        .gap_x_1()
1314                        .children(Self::render_icon(
1315                            has_left_icon,
1316                            is_left_check,
1317                            icon.clone(),
1318                            window,
1319                            cx,
1320                        ))
1321                        .child((render)(window, cx))
1322                        .children(right_check_icon.map(|icon| icon.ml_3())),
1323                ),
1324            PopupMenuItem::Item {
1325                icon,
1326                label,
1327                action,
1328                disabled,
1329                is_link,
1330                ..
1331            } => {
1332                let show_link_icon = *is_link && self.external_link_icon;
1333                let action = action.as_ref().map(|action| action.boxed_clone());
1334                let key = self.render_key_binding(action, window, cx);
1335
1336                this.when(!disabled, |this| {
1337                    this.on_click(cx.listener(move |this, event, window, cx| {
1338                        this.on_click(ix, event, window, cx)
1339                    }))
1340                })
1341                .disabled(*disabled)
1342                .h(item_height)
1343                .gap_x_1()
1344                .children(Self::render_icon(
1345                    has_left_icon,
1346                    is_left_check,
1347                    icon.clone(),
1348                    window,
1349                    cx,
1350                ))
1351                .child(
1352                    h_flex()
1353                        .w_full()
1354                        .gap_3()
1355                        .items_center()
1356                        .justify_between()
1357                        .when(!show_link_icon, |this| this.child(label.clone()))
1358                        .children(right_check_icon)
1359                        .when(show_link_icon, |this| {
1360                            this.child(
1361                                h_flex()
1362                                    .w_full()
1363                                    .justify_between()
1364                                    .gap_1p5()
1365                                    .child(label.clone())
1366                                    .child(
1367                                        Icon::new(IconName::ExternalLink)
1368                                            .xsmall()
1369                                            .text_color(cx.theme().muted_foreground),
1370                                    ),
1371                            )
1372                        })
1373                        .children(key),
1374                )
1375            }
1376            PopupMenuItem::Submenu {
1377                icon,
1378                label,
1379                menu,
1380                disabled,
1381            } => this
1382                .selected(selected)
1383                .disabled(*disabled)
1384                .items_start()
1385                .child(
1386                    h_flex()
1387                        .min_h(item_height)
1388                        .size_full()
1389                        .items_center()
1390                        .gap_x_1()
1391                        .children(Self::render_icon(
1392                            has_left_icon,
1393                            false,
1394                            icon.clone(),
1395                            window,
1396                            cx,
1397                        ))
1398                        .child(
1399                            h_flex()
1400                                .flex_1()
1401                                .gap_2()
1402                                .items_center()
1403                                .justify_between()
1404                                .child(label.clone())
1405                                .child(
1406                                    Icon::new(IconName::ChevronRight)
1407                                        .xsmall()
1408                                        .text_color(cx.theme().muted_foreground),
1409                                ),
1410                        ),
1411                )
1412                .when(selected, |this| {
1413                    this.child({
1414                        let (anchor, left) = self.submenu_anchor;
1415                        let is_bottom_pos =
1416                            matches!(anchor, Anchor::BottomLeft | Anchor::BottomRight);
1417                        deferred(
1418                            anchored()
1419                                .anchor(anchor)
1420                                .child(
1421                                    div()
1422                                        .id("submenu")
1423                                        .test_support()
1424                                        .occlude()
1425                                        .when(is_bottom_pos, |this| this.bottom_0())
1426                                        .when(!is_bottom_pos, |this| this.top_neg_1())
1427                                        .left(left)
1428                                        .child(menu.clone()),
1429                                )
1430                                .snap_to_window_with_margin(Edges::all(EDGE_PADDING)),
1431                        )
1432                        .with_priority(self.priority + 1)
1433                    })
1434                }),
1435        }
1436    }
1437}
1438
1439impl FluentBuilder for PopupMenu {}
1440impl EventEmitter<DismissEvent> for PopupMenu {}
1441impl Focusable for PopupMenu {
1442    fn focus_handle(&self, _: &App) -> FocusHandle {
1443        self.focus_handle.clone()
1444    }
1445}
1446
1447#[derive(Clone, Copy)]
1448struct RenderOptions {
1449    has_left_icon: bool,
1450    check_side: Side,
1451    radius: Pixels,
1452}
1453
1454impl Render for PopupMenu {
1455    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1456        self.update_submenu_menu_anchor(window);
1457
1458        // Submenus attached via the public `item()` + `PopupMenuItem::submenu()`
1459        // path (from contexts that only have the menu value, e.g. a table
1460        // delegate's `context_menu`) have no parent wired at construction time.
1461        // Wire them here so the dismiss chain, click-outside checks and keyboard
1462        // navigation treat them the same as `submenu()`-built children.
1463        let parent = cx.entity().downgrade();
1464        let parent_priority = self.priority;
1465        for item in &self.menu_items {
1466            if let PopupMenuItem::Submenu { menu, .. } = item {
1467                if menu.read(cx).parent_menu.is_none() {
1468                    menu.update(cx, |menu, _| {
1469                        menu.parent_menu = Some(parent.clone());
1470                        menu.priority = parent_priority + 1;
1471                    });
1472                }
1473            }
1474        }
1475
1476        let view = cx.entity().clone();
1477        let items_count = self.menu_items.len();
1478
1479        let max_height = self.max_height.unwrap_or_else(|| {
1480            let window_half_height = window.window_bounds().get_bounds().size.height * 0.5;
1481            window_half_height.min(px(450.))
1482        });
1483
1484        let has_left_icon = self
1485            .menu_items
1486            .iter()
1487            .any(|item| item.has_left_icon(self.check_side));
1488
1489        let max_width = self.max_width();
1490        let options = RenderOptions {
1491            has_left_icon,
1492            check_side: self.check_side,
1493            radius: cx.theme().radius.min(px(8.)),
1494        };
1495
1496        v_flex()
1497            .id("popup-menu")
1498            .test_support()
1499            .role(Role::Menu)
1500            .key_context(CONTEXT)
1501            .track_focus(&self.focus_handle)
1502            .on_action(cx.listener(Self::select_up))
1503            .on_action(cx.listener(Self::select_down))
1504            .on_action(cx.listener(Self::select_left))
1505            .on_action(cx.listener(Self::select_right))
1506            .on_action(cx.listener(Self::confirm))
1507            .on_action(cx.listener(Self::dismiss))
1508            .on_mouse_down_out(cx.listener(Self::on_mouse_down_out))
1509            .popover_style(cx)
1510            .text_color(cx.theme().popover_foreground)
1511            .relative()
1512            .occlude()
1513            .child(
1514                v_flex()
1515                    .id("items")
1516                    .p_1()
1517                    .gap_y_0p5()
1518                    .min_w(rems(8.))
1519                    .when_some(self.min_width, |this, min_width| this.min_w(min_width))
1520                    .max_w(max_width)
1521                    .when(self.scrollable, |this| {
1522                        this.max_h(max_height)
1523                            .overflow_y_scroll()
1524                            .track_scroll(&self.scroll_handle)
1525                    })
1526                    .children(
1527                        self.menu_items
1528                            .iter()
1529                            .enumerate()
1530                            // Ignore last separator
1531                            .filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
1532                            .map(|(ix, item)| self.render_item(ix, item, options, window, cx)),
1533                    )
1534                    .on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)),
1535            )
1536            .when(self.scrollable, |this| {
1537                this.vertical_scrollbar(&self.scroll_handle)
1538            })
1539    }
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544    use super::*;
1545
1546    #[gpui::test]
1547    fn custom_menu_item_preserves_mouse_event(cx: &mut gpui::TestAppContext) {
1548        use gpui::{Modifiers, MouseButton, MouseUpEvent};
1549        use std::cell::RefCell;
1550
1551        cx.update(crate::init);
1552        let events = Rc::new(RefCell::new(Vec::new()));
1553        let (_, cx) = cx.add_window_view({
1554            let events = events.clone();
1555            move |_, cx| {
1556                PopupMenu::new(cx).item(
1557                    PopupMenuItem::element(|_, _| {
1558                        div()
1559                            .debug_selector(|| "custom-menu-content".into())
1560                            .w(px(100.))
1561                            .h(px(20.))
1562                            .child("Custom")
1563                    })
1564                    .on_click(move |event, _, _| events.borrow_mut().push(event.clone())),
1565                )
1566            }
1567        });
1568        cx.update(|window, cx| window.draw(cx).clear(cx));
1569        let position = cx.debug_bounds("custom-menu-content").unwrap().center();
1570        let modifiers = Modifiers {
1571            shift: true,
1572            ..Modifiers::default()
1573        };
1574        cx.simulate_mouse_down(position, MouseButton::Left, modifiers);
1575        cx.simulate_event(MouseUpEvent {
1576            position,
1577            button: MouseButton::Left,
1578            modifiers,
1579            click_count: 2,
1580        });
1581        cx.run_until_parked();
1582        let events = events.borrow();
1583        assert_eq!(events.len(), 1);
1584        assert_eq!(events[0].mouse_position(), Some(position));
1585        assert_eq!(events[0].modifiers(), modifiers);
1586        let ClickEvent::Mouse(event) = &events[0] else {
1587            panic!("expected the original mouse click");
1588        };
1589        assert_eq!(event.up.click_count, 2);
1590    }
1591
1592    #[gpui::test]
1593    fn popup_menu_item_a11y_label_uses_visible_label(cx: &mut gpui::TestAppContext) {
1594        let submenu = cx.update(|cx| cx.new(|cx| PopupMenu::new(cx)));
1595
1596        assert_eq!(PopupMenuItem::new("Open").a11y_label(), Some("Open".into()));
1597        assert_eq!(
1598            PopupMenuItem::link("Docs", "https://example.com").a11y_label(),
1599            Some("Docs".into())
1600        );
1601        assert_eq!(
1602            PopupMenuItem::label("Recent files").a11y_label(),
1603            Some("Recent files".into())
1604        );
1605        assert_eq!(
1606            PopupMenuItem::submenu("More", submenu).a11y_label(),
1607            Some("More".into())
1608        );
1609        assert_eq!(PopupMenuItem::separator().a11y_label(), None);
1610        assert_eq!(PopupMenuItem::element(|_, _| div()).a11y_label(), None);
1611    }
1612}