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