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    pub fn action_context(mut self, handle: FocusHandle) -> Self {
372        self.action_context = Some(handle);
373        self
374    }
375
376    pub(crate) fn set_action_context(
377        &mut self,
378        action_context: Option<FocusHandle>,
379        cx: &mut Context<Self>,
380    ) {
381        self.action_context = action_context.clone();
382
383        for item in &self.menu_items {
384            if let PopupMenuItem::Submenu { menu, .. } = item {
385                menu.update(cx, |menu, cx| {
386                    menu.set_action_context(action_context.clone(), cx);
387                });
388            }
389        }
390    }
391
392    /// Set the focus to restore when the menu is dismissed, without changing
393    /// where actions are dispatched.
394    pub(crate) fn set_previous_focus(
395        &mut self,
396        handle: Option<FocusHandle>,
397        cx: &mut Context<Self>,
398    ) {
399        self.previous_focus_handle = handle.clone();
400
401        for item in &self.menu_items {
402            if let PopupMenuItem::Submenu { menu, .. } = item {
403                menu.update(cx, |menu, cx| {
404                    menu.set_previous_focus(handle.clone(), cx);
405                });
406            }
407        }
408    }
409
410    /// Set the focus handle on the trigger's dispatch path that shortcut hints
411    /// resolve against, without changing focus or where actions are dispatched.
412    pub(crate) fn set_trigger_focus(
413        &mut self,
414        handle: Option<FocusHandle>,
415        cx: &mut Context<Self>,
416    ) {
417        self.trigger_focus_handle = handle.clone();
418
419        for item in &self.menu_items {
420            if let PopupMenuItem::Submenu { menu, .. } = item {
421                menu.update(cx, |menu, cx| {
422                    menu.set_trigger_focus(handle.clone(), cx);
423                });
424            }
425        }
426    }
427
428    /// Set min width of the popup menu, default is 120px
429    pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
430        self.min_width = Some(width.into());
431        self
432    }
433
434    /// Set max width of the popup menu, default is 500px
435    pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
436        self.max_width = Some(width.into());
437        self
438    }
439
440    /// Set max height of the popup menu, default is half of the window height
441    pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
442        self.max_height = Some(height.into());
443        self
444    }
445
446    /// Set the menu to be scrollable to show vertical scrollbar.
447    pub fn scrollable(mut self, scrollable: bool) -> Self {
448        self.scrollable = scrollable;
449        self
450    }
451
452    /// Set the side to show check icon, default is `Side::Left`.
453    pub fn check_side(mut self, side: Side) -> Self {
454        self.check_side = side;
455        self
456    }
457
458    /// Set the menu to show external link icon, default is true.
459    pub fn external_link_icon(mut self, visible: bool) -> Self {
460        self.external_link_icon = visible;
461        self
462    }
463
464    /// Add Menu Item
465    pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
466        self.menu_with_disabled(label, action, false)
467    }
468
469    /// Add Menu Item with enable state
470    pub fn menu_with_enable(
471        mut self,
472        label: impl Into<SharedString>,
473        action: Box<dyn Action>,
474        enable: bool,
475    ) -> Self {
476        self.add_menu_item(label, None, action, !enable, false);
477        self
478    }
479
480    /// Add Menu Item with disabled state
481    pub fn menu_with_disabled(
482        mut self,
483        label: impl Into<SharedString>,
484        action: Box<dyn Action>,
485        disabled: bool,
486    ) -> Self {
487        self.add_menu_item(label, None, action, disabled, false);
488        self
489    }
490
491    /// Add label
492    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
493        self.menu_items.push(PopupMenuItem::label(label.into()));
494        self
495    }
496
497    /// Add Menu to open link
498    pub fn link(self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
499        self.link_with_disabled(label, href, false)
500    }
501
502    /// Add Menu to open link with disabled state
503    pub fn link_with_disabled(
504        mut self,
505        label: impl Into<SharedString>,
506        href: impl Into<String>,
507        disabled: bool,
508    ) -> Self {
509        let href = href.into();
510        self.menu_items
511            .push(PopupMenuItem::link(label, href).disabled(disabled));
512        self
513    }
514
515    /// Add Menu to open link
516    pub fn link_with_icon(
517        self,
518        label: impl Into<SharedString>,
519        icon: impl Into<Icon>,
520        href: impl Into<String>,
521    ) -> Self {
522        self.link_with_icon_and_disabled(label, icon, href, false)
523    }
524
525    /// Add Menu to open link with icon and disabled state
526    fn link_with_icon_and_disabled(
527        mut self,
528        label: impl Into<SharedString>,
529        icon: impl Into<Icon>,
530        href: impl Into<String>,
531        disabled: bool,
532    ) -> Self {
533        let href = href.into();
534        self.menu_items.push(
535            PopupMenuItem::link(label, href)
536                .icon(icon)
537                .disabled(disabled),
538        );
539        self
540    }
541
542    /// Add Menu Item with Icon.
543    pub fn menu_with_icon(
544        self,
545        label: impl Into<SharedString>,
546        icon: impl Into<Icon>,
547        action: Box<dyn Action>,
548    ) -> Self {
549        self.menu_with_icon_and_disabled(label, icon, action, false)
550    }
551
552    /// Add Menu Item with Icon and disabled state
553    pub fn menu_with_icon_and_disabled(
554        mut self,
555        label: impl Into<SharedString>,
556        icon: impl Into<Icon>,
557        action: Box<dyn Action>,
558        disabled: bool,
559    ) -> Self {
560        self.add_menu_item(label, Some(icon.into()), action, disabled, false);
561        self
562    }
563
564    /// Add Menu Item with check icon
565    pub fn menu_with_check(
566        self,
567        label: impl Into<SharedString>,
568        checked: bool,
569        action: Box<dyn Action>,
570    ) -> Self {
571        self.menu_with_check_and_disabled(label, checked, action, false)
572    }
573
574    /// Add Menu Item with check icon and disabled state
575    pub fn menu_with_check_and_disabled(
576        mut self,
577        label: impl Into<SharedString>,
578        checked: bool,
579        action: Box<dyn Action>,
580        disabled: bool,
581    ) -> Self {
582        self.add_menu_item(label, None, action, disabled, checked);
583        self
584    }
585
586    /// Add Menu Item with custom element render.
587    pub fn menu_element<F, E>(self, action: Box<dyn Action>, builder: F) -> Self
588    where
589        F: Fn(&mut Window, &mut App) -> E + 'static,
590        E: IntoElement,
591    {
592        self.menu_element_with_check(false, action, builder)
593    }
594
595    /// Add Menu Item with custom element render with disabled state.
596    pub fn menu_element_with_disabled<F, E>(
597        self,
598        action: Box<dyn Action>,
599        disabled: bool,
600        builder: F,
601    ) -> Self
602    where
603        F: Fn(&mut Window, &mut App) -> E + 'static,
604        E: IntoElement,
605    {
606        self.menu_element_with_check_and_disabled(false, action, disabled, builder)
607    }
608
609    /// Add Menu Item with custom element render with icon.
610    pub fn menu_element_with_icon<F, E>(
611        self,
612        icon: impl Into<Icon>,
613        action: Box<dyn Action>,
614        builder: F,
615    ) -> Self
616    where
617        F: Fn(&mut Window, &mut App) -> E + 'static,
618        E: IntoElement,
619    {
620        self.menu_element_with_icon_and_disabled(icon, action, false, builder)
621    }
622
623    /// Add Menu Item with custom element render with check state
624    pub fn menu_element_with_check<F, E>(
625        self,
626        checked: bool,
627        action: Box<dyn Action>,
628        builder: F,
629    ) -> Self
630    where
631        F: Fn(&mut Window, &mut App) -> E + 'static,
632        E: IntoElement,
633    {
634        self.menu_element_with_check_and_disabled(checked, action, false, builder)
635    }
636
637    /// Add Menu Item with custom element render with icon and disabled state
638    fn menu_element_with_icon_and_disabled<F, E>(
639        mut self,
640        icon: impl Into<Icon>,
641        action: Box<dyn Action>,
642        disabled: bool,
643        builder: F,
644    ) -> Self
645    where
646        F: Fn(&mut Window, &mut App) -> E + 'static,
647        E: IntoElement,
648    {
649        self.menu_items.push(
650            PopupMenuItem::element(builder)
651                .action(action)
652                .icon(icon)
653                .disabled(disabled),
654        );
655        self
656    }
657
658    /// Add Menu Item with custom element render with check state and disabled state
659    fn menu_element_with_check_and_disabled<F, E>(
660        mut self,
661        checked: bool,
662        action: Box<dyn Action>,
663        disabled: bool,
664        builder: F,
665    ) -> Self
666    where
667        F: Fn(&mut Window, &mut App) -> E + 'static,
668        E: IntoElement,
669    {
670        self.menu_items.push(
671            PopupMenuItem::element(builder)
672                .action(action)
673                .checked(checked)
674                .disabled(disabled),
675        );
676        self
677    }
678
679    /// Add a separator Menu Item
680    pub fn separator(mut self) -> Self {
681        if self.menu_items.is_empty() {
682            return self;
683        }
684
685        if let Some(PopupMenuItem::Separator) = self.menu_items.last() {
686            return self;
687        }
688
689        self.menu_items.push(PopupMenuItem::separator());
690        self
691    }
692
693    /// Add a Submenu
694    pub fn submenu(
695        self,
696        label: impl Into<SharedString>,
697        window: &mut Window,
698        cx: &mut Context<Self>,
699        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
700    ) -> Self {
701        self.submenu_with_icon(None, label, window, cx, f)
702    }
703
704    /// Add a Submenu item with icon
705    pub fn submenu_with_icon(
706        mut self,
707        icon: Option<Icon>,
708        label: impl Into<SharedString>,
709        window: &mut Window,
710        cx: &mut Context<Self>,
711        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
712    ) -> Self {
713        let submenu = PopupMenu::build(window, cx, f);
714        let parent_menu = cx.entity().downgrade();
715        let parent_priority = self.priority;
716        submenu.update(cx, |view, _| {
717            view.parent_menu = Some(parent_menu);
718            view.priority = parent_priority + 1;
719        });
720
721        self.menu_items.push(
722            PopupMenuItem::submenu(label, submenu).when_some(icon, |this, icon| this.icon(icon)),
723        );
724        self
725    }
726
727    /// Add menu item.
728    pub fn item(mut self, item: impl Into<PopupMenuItem>) -> Self {
729        let item: PopupMenuItem = item.into();
730        self.menu_items.push(item);
731        self
732    }
733
734    /// Replace all menu items by re-running a builder on this menu, keeping its
735    /// identity (focus, parent menu, layer priority).
736    ///
737    /// For menus whose content arrives asynchronously after the menu is shown,
738    /// e.g. swapping a "loading…" placeholder for the loaded items:
739    ///
740    /// ```ignore
741    /// cx.spawn_in(window, async move |menu, cx| {
742    ///     let items = fetch_items().await;
743    ///     _ = menu.update_in(cx, |menu, window, cx| {
744    ///         menu.rebuild(window, cx, |menu, _, _| {
745    ///             items.into_iter().fold(menu, |menu, item| {
746    ///                 menu.menu(item.label, Box::new(item.action))
747    ///             })
748    ///         });
749    ///     });
750    /// })
751    /// .detach();
752    /// ```
753    pub fn rebuild(
754        &mut self,
755        window: &mut Window,
756        cx: &mut Context<Self>,
757        f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
758    ) {
759        let mut menu = std::mem::replace(self, Self::new(cx));
760        menu.menu_items.clear();
761        menu.selected_index = None;
762        *self = f(menu, window, cx);
763        cx.notify();
764    }
765
766    fn add_menu_item(
767        &mut self,
768        label: impl Into<SharedString>,
769        icon: Option<Icon>,
770        action: Box<dyn Action>,
771        disabled: bool,
772        checked: bool,
773    ) -> &mut Self {
774        self.menu_items.push(
775            PopupMenuItem::new(label)
776                .when_some(icon, |item, icon| item.icon(icon))
777                .disabled(disabled)
778                .checked(checked)
779                .action(action),
780        );
781        self
782    }
783
784    pub(super) fn with_menu_items<I>(
785        mut self,
786        items: impl IntoIterator<Item = I>,
787        window: &mut Window,
788        cx: &mut Context<Self>,
789    ) -> Self
790    where
791        I: Into<OwnedMenuItem>,
792    {
793        for item in items {
794            match item.into() {
795                OwnedMenuItem::Action {
796                    name,
797                    action,
798                    checked,
799                    disabled,
800                    ..
801                } => {
802                    self = self.menu_with_check_and_disabled(
803                        name,
804                        checked,
805                        action.boxed_clone(),
806                        disabled,
807                    )
808                }
809                OwnedMenuItem::Separator => {
810                    self = self.separator();
811                }
812                OwnedMenuItem::Submenu(submenu) => {
813                    self = self.submenu(submenu.name, window, cx, move |menu, window, cx| {
814                        menu.with_menu_items(submenu.items.clone(), window, cx)
815                    })
816                }
817                OwnedMenuItem::SystemMenu(_) => {}
818            }
819        }
820
821        if self.menu_items.len() > 20 {
822            self.scrollable = true;
823        }
824
825        self
826    }
827
828    pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
829        if let Some(ix) = self.selected_index {
830            if let Some(item) = self.menu_items.get(ix) {
831                return match item {
832                    PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
833                    _ => None,
834                };
835            }
836        }
837
838        None
839    }
840
841    pub fn is_empty(&self) -> bool {
842        self.menu_items.is_empty()
843    }
844
845    fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
846        self.menu_items
847            .iter()
848            .enumerate()
849            .filter(|(_, item)| item.is_clickable())
850    }
851
852    fn on_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
853        cx.stop_propagation();
854        window.prevent_default();
855        self.selected_index = Some(ix);
856        self.confirm(&Confirm { secondary: false }, window, cx);
857    }
858
859    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
860        match self.selected_index {
861            Some(index) => {
862                let item = self.menu_items.get(index);
863                match item {
864                    Some(PopupMenuItem::Item {
865                        handler, action, ..
866                    }) => {
867                        if let Some(handler) = handler {
868                            handler(&ClickEvent::default(), window, cx);
869                        } else if let Some(action) = action.as_ref() {
870                            self.dispatch_confirm_action(action, window, cx);
871                        }
872
873                        self.dismiss(&Cancel, window, cx)
874                    }
875                    Some(PopupMenuItem::ElementItem {
876                        handler, action, ..
877                    }) => {
878                        if let Some(handler) = handler {
879                            handler(&ClickEvent::default(), window, cx);
880                        } else if let Some(action) = action.as_ref() {
881                            self.dispatch_confirm_action(action, window, cx);
882                        }
883                        self.dismiss(&Cancel, window, cx)
884                    }
885                    _ => {}
886                }
887            }
888            _ => {}
889        }
890    }
891
892    fn dispatch_confirm_action(
893        &self,
894        action: &Box<dyn Action>,
895        window: &mut Window,
896        cx: &mut Context<Self>,
897    ) {
898        if let Some(context) = self.action_context.as_ref() {
899            context.focus(window, cx);
900        }
901
902        window.dispatch_action(action.boxed_clone(), cx);
903    }
904
905    fn set_selected_index(&mut self, ix: usize, cx: &mut Context<Self>) {
906        if self.selected_index != Some(ix) {
907            self.selected_index = Some(ix);
908            self.scroll_handle.scroll_to_item(ix);
909            cx.notify();
910        }
911    }
912
913    fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
914        cx.stop_propagation();
915        let ix = self.selected_index.unwrap_or(0);
916
917        if let Some((prev_ix, _)) = self
918            .menu_items
919            .iter()
920            .enumerate()
921            .rev()
922            .find(|(i, item)| *i < ix && item.is_clickable())
923        {
924            self.set_selected_index(prev_ix, cx);
925            return;
926        }
927
928        let last_clickable_ix = self.clickable_menu_items().last().map(|(ix, _)| ix);
929        self.set_selected_index(last_clickable_ix.unwrap_or(0), cx);
930    }
931
932    fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
933        cx.stop_propagation();
934        let Some(ix) = self.selected_index else {
935            self.set_selected_index(0, cx);
936            return;
937        };
938
939        if let Some((next_ix, _)) = self
940            .menu_items
941            .iter()
942            .enumerate()
943            .find(|(i, item)| *i > ix && item.is_clickable())
944        {
945            self.set_selected_index(next_ix, cx);
946            return;
947        }
948
949        self.set_selected_index(0, cx);
950    }
951
952    fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
953        let handled = if matches!(self.submenu_anchor.0, Anchor::TopLeft | Anchor::BottomLeft) {
954            self._unselect_submenu(window, cx)
955        } else {
956            self._select_submenu(window, cx)
957        };
958
959        if self.parent_side(cx).is_left() {
960            self._focus_parent_menu(window, cx);
961        }
962
963        if handled {
964            return;
965        }
966
967        // For parent AppMenuBar to handle.
968        if self.parent_menu.is_none() {
969            cx.propagate();
970        }
971    }
972
973    fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
974        let handled = if matches!(self.submenu_anchor.0, Anchor::TopLeft | Anchor::BottomLeft) {
975            self._select_submenu(window, cx)
976        } else {
977            self._unselect_submenu(window, cx)
978        };
979
980        if self.parent_side(cx).is_right() {
981            self._focus_parent_menu(window, cx);
982        }
983
984        if handled {
985            return;
986        }
987
988        // For parent AppMenuBar to handle.
989        if self.parent_menu.is_none() {
990            cx.propagate();
991        }
992    }
993
994    fn _select_submenu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
995        if let Some(active_submenu) = self.active_submenu() {
996            // Focus the submenu, so that can be handle the action.
997            active_submenu.update(cx, |view, cx| {
998                view.set_selected_index(0, cx);
999                view.focus_handle.focus(window, cx);
1000            });
1001            cx.notify();
1002            return true;
1003        }
1004
1005        return false;
1006    }
1007
1008    fn _unselect_submenu(&mut self, _: &mut Window, cx: &mut Context<Self>) -> bool {
1009        if let Some(active_submenu) = self.active_submenu() {
1010            active_submenu.update(cx, |view, cx| {
1011                view.selected_index = None;
1012                cx.notify();
1013            });
1014            return true;
1015        }
1016
1017        return false;
1018    }
1019
1020    fn _focus_parent_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1021        let Some(parent) = self.parent_menu.as_ref() else {
1022            return;
1023        };
1024        let Some(parent) = parent.upgrade() else {
1025            return;
1026        };
1027
1028        self.selected_index = None;
1029        parent.update(cx, |view, cx| {
1030            view.focus_handle.focus(window, cx);
1031            cx.notify();
1032        });
1033    }
1034
1035    fn parent_side(&self, cx: &App) -> Side {
1036        let Some(parent) = self.parent_menu.as_ref() else {
1037            return Side::Left;
1038        };
1039
1040        let Some(parent) = parent.upgrade() else {
1041            return Side::Left;
1042        };
1043
1044        match parent.read(cx).submenu_anchor.0 {
1045            Anchor::TopLeft | Anchor::BottomLeft => Side::Left,
1046            Anchor::TopRight | Anchor::BottomRight => Side::Right,
1047            // Center anchors are not used for submenu positioning, but we must cover them.
1048            _ => Side::Left,
1049        }
1050    }
1051
1052    /// Dismiss the menu and the entire parent chain.
1053    ///
1054    /// The submenu is closed together with its parent, same as macOS menus.
1055    fn dismiss(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
1056        self.selected_index = None;
1057        cx.emit(DismissEvent);
1058
1059        // Focus back to the previous focused handle, unless the item's click
1060        // handler has already moved focus elsewhere (e.g. opened a dialog and
1061        // focused its input) -- stealing focus back would break that.
1062        let focus_moved_away =
1063            window.focused(cx).is_some() && !self.focus_handle.contains_focused(window, cx);
1064        if !focus_moved_away {
1065            if let Some(handle) = self
1066                .previous_focus_handle
1067                .as_ref()
1068                .or(self.action_context.as_ref())
1069            {
1070                window.focus(handle, cx);
1071            }
1072        }
1073
1074        let Some(parent_menu) = self.parent_menu.clone() else {
1075            return;
1076        };
1077
1078        // Dismiss parent menu, when this menu is dismissed
1079        _ = parent_menu.update(cx, |view, cx| {
1080            view.dismiss(&Cancel, window, cx);
1081        });
1082    }
1083
1084    fn handle_dismiss(
1085        &mut self,
1086        position: &Point<Pixels>,
1087        window: &mut Window,
1088        cx: &mut Context<Self>,
1089    ) {
1090        // Do not dismiss, if click inside the parent menu
1091        if let Some(parent) = self.parent_menu.as_ref() {
1092            if let Some(parent) = parent.upgrade() {
1093                if parent.read(cx).bounds.contains(position) {
1094                    return;
1095                }
1096            }
1097        }
1098
1099        // Do not dismiss, if there have an active submenu, the click may be
1100        // inside the submenu, let the submenu to handle it.
1101        //
1102        // Otherwise the submenu will be dismissed before its item's `on_click`.
1103        if self.active_submenu().is_some() {
1104            return;
1105        }
1106
1107        self.dismiss(&Cancel, window, cx);
1108    }
1109
1110    fn on_mouse_down_out(
1111        &mut self,
1112        e: &MouseDownEvent,
1113        window: &mut Window,
1114        cx: &mut Context<Self>,
1115    ) {
1116        self.handle_dismiss(&e.position, window, cx);
1117    }
1118
1119    fn render_key_binding(
1120        &self,
1121        action: Option<Box<dyn Action>>,
1122        window: &mut Window,
1123        _: &mut Context<Self>,
1124    ) -> Option<Kbd> {
1125        let action = action?;
1126
1127        // Each handle names a dispatch path GPUI can resolve in the previously
1128        // rendered frame: the explicit action target, the trigger the menu
1129        // opened from (already rendered when the menu first draws), the focus
1130        // the menu interrupted, then the menu's own path once it has been
1131        // rendered. A binding registered without a key context applies on
1132        // every path, so it is the last resort rather than the window's
1133        // leftover context stack, which only holds the path of whatever
1134        // element happened to paint last.
1135        [
1136            self.action_context.as_ref(),
1137            self.trigger_focus_handle.as_ref(),
1138            self.previous_focus_handle.as_ref(),
1139            Some(&self.focus_handle),
1140        ]
1141        .into_iter()
1142        .flatten()
1143        .find_map(|handle| Kbd::binding_for_action_in(action.as_ref(), handle, window))
1144        .or_else(|| Kbd::global_binding_for_action(action.as_ref(), window))
1145        .map(|this| {
1146            this.p_0()
1147                .flex_nowrap()
1148                .border_0()
1149                .bg(gpui::transparent_white())
1150        })
1151    }
1152
1153    fn render_icon(
1154        has_icon: bool,
1155        checked: bool,
1156        icon: Option<Icon>,
1157        _: &mut Window,
1158        _: &mut Context<Self>,
1159    ) -> Option<impl IntoElement> {
1160        if !has_icon {
1161            return None;
1162        }
1163
1164        let icon = if let Some(icon) = icon {
1165            icon.clone()
1166        } else if checked {
1167            Icon::new(IconName::Check)
1168        } else {
1169            Icon::empty()
1170        };
1171
1172        Some(icon.xsmall())
1173    }
1174
1175    #[inline]
1176    fn max_width(&self) -> Pixels {
1177        self.max_width.unwrap_or(px(500.))
1178    }
1179
1180    /// Calculate the anchor corner and left offset for child submenu
1181    fn update_submenu_menu_anchor(&mut self, window: &Window) {
1182        let bounds = self.bounds;
1183        let max_width = self.max_width();
1184        let (anchor, left) = if max_width + bounds.origin.x > window.bounds().size.width {
1185            (Anchor::TopRight, -px(16.))
1186        } else {
1187            (Anchor::TopLeft, bounds.size.width - px(8.))
1188        };
1189
1190        let is_bottom_pos = bounds.origin.y + bounds.size.height > window.bounds().size.height;
1191        self.submenu_anchor = if is_bottom_pos {
1192            (anchor.other_side_along(gpui::Axis::Vertical), left)
1193        } else {
1194            (anchor, left)
1195        };
1196    }
1197
1198    fn render_item(
1199        &self,
1200        ix: usize,
1201        item: &PopupMenuItem,
1202        options: RenderOptions,
1203        window: &mut Window,
1204        cx: &mut Context<Self>,
1205    ) -> MenuItemElement {
1206        let has_left_icon = options.has_left_icon;
1207        let is_left_check = options.check_side.is_left() && item.is_checked();
1208        let right_check_icon = if options.check_side.is_right() && item.is_checked() {
1209            Some(Icon::new(IconName::Check).xsmall())
1210        } else {
1211            None
1212        };
1213
1214        let selected = self.selected_index == Some(ix);
1215        const EDGE_PADDING: Pixels = px(4.);
1216        const INNER_PADDING: Pixels = px(8.);
1217
1218        let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
1219        let group_name = format!("{}:item-{}", cx.entity().entity_id(), ix);
1220
1221        let (item_height, radius) = match self.size {
1222            Size::Small => (px(20.), options.radius.half()),
1223            _ => (px(26.), options.radius),
1224        };
1225
1226        let this = MenuItemElement::new(ix, &group_name)
1227            .relative()
1228            .text_sm()
1229            .py_0()
1230            .px(INNER_PADDING)
1231            .rounded(radius)
1232            .items_center()
1233            .selected(selected)
1234            .on_hover(cx.listener(move |this, hovered, _, cx| {
1235                if *hovered {
1236                    this.selected_index = Some(ix);
1237                } else if !is_submenu && this.selected_index == Some(ix) {
1238                    // TODO: Better handle the submenu unselection when hover out
1239                    this.selected_index = None;
1240                }
1241
1242                cx.notify();
1243            }))
1244            .when_some(item.a11y_label(), |this, label| this.aria_label(label));
1245
1246        match item {
1247            PopupMenuItem::Separator => this
1248                .h_auto()
1249                .p_0()
1250                .my_0p5()
1251                .mx_neg_1()
1252                .border_b(px(2.))
1253                .border_color(cx.theme().border)
1254                .disabled(true),
1255            PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
1256                h_flex()
1257                    .cursor_default()
1258                    .items_center()
1259                    .gap_x_1()
1260                    .children(Self::render_icon(has_left_icon, false, None, window, cx))
1261                    .child(div().flex_1().child(label.clone())),
1262            ),
1263            PopupMenuItem::ElementItem {
1264                render,
1265                icon,
1266                disabled,
1267                ..
1268            } => this
1269                .when(!disabled, |this| {
1270                    this.on_click(
1271                        cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
1272                    )
1273                })
1274                .disabled(*disabled)
1275                .child(
1276                    h_flex()
1277                        .flex_1()
1278                        .min_h(item_height)
1279                        .items_center()
1280                        .gap_x_1()
1281                        .children(Self::render_icon(
1282                            has_left_icon,
1283                            is_left_check,
1284                            icon.clone(),
1285                            window,
1286                            cx,
1287                        ))
1288                        .child((render)(window, cx))
1289                        .children(right_check_icon.map(|icon| icon.ml_3())),
1290                ),
1291            PopupMenuItem::Item {
1292                icon,
1293                label,
1294                action,
1295                disabled,
1296                is_link,
1297                ..
1298            } => {
1299                let show_link_icon = *is_link && self.external_link_icon;
1300                let action = action.as_ref().map(|action| action.boxed_clone());
1301                let key = self.render_key_binding(action, window, cx);
1302
1303                this.when(!disabled, |this| {
1304                    this.on_click(
1305                        cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
1306                    )
1307                })
1308                .disabled(*disabled)
1309                .h(item_height)
1310                .gap_x_1()
1311                .children(Self::render_icon(
1312                    has_left_icon,
1313                    is_left_check,
1314                    icon.clone(),
1315                    window,
1316                    cx,
1317                ))
1318                .child(
1319                    h_flex()
1320                        .w_full()
1321                        .gap_3()
1322                        .items_center()
1323                        .justify_between()
1324                        .when(!show_link_icon, |this| this.child(label.clone()))
1325                        .children(right_check_icon)
1326                        .when(show_link_icon, |this| {
1327                            this.child(
1328                                h_flex()
1329                                    .w_full()
1330                                    .justify_between()
1331                                    .gap_1p5()
1332                                    .child(label.clone())
1333                                    .child(
1334                                        Icon::new(IconName::ExternalLink)
1335                                            .xsmall()
1336                                            .text_color(cx.theme().muted_foreground),
1337                                    ),
1338                            )
1339                        })
1340                        .children(key),
1341                )
1342            }
1343            PopupMenuItem::Submenu {
1344                icon,
1345                label,
1346                menu,
1347                disabled,
1348            } => this
1349                .selected(selected)
1350                .disabled(*disabled)
1351                .items_start()
1352                .child(
1353                    h_flex()
1354                        .min_h(item_height)
1355                        .size_full()
1356                        .items_center()
1357                        .gap_x_1()
1358                        .children(Self::render_icon(
1359                            has_left_icon,
1360                            false,
1361                            icon.clone(),
1362                            window,
1363                            cx,
1364                        ))
1365                        .child(
1366                            h_flex()
1367                                .flex_1()
1368                                .gap_2()
1369                                .items_center()
1370                                .justify_between()
1371                                .child(label.clone())
1372                                .child(
1373                                    Icon::new(IconName::ChevronRight)
1374                                        .xsmall()
1375                                        .text_color(cx.theme().muted_foreground),
1376                                ),
1377                        ),
1378                )
1379                .when(selected, |this| {
1380                    this.child({
1381                        let (anchor, left) = self.submenu_anchor;
1382                        let is_bottom_pos =
1383                            matches!(anchor, Anchor::BottomLeft | Anchor::BottomRight);
1384                        deferred(
1385                            anchored()
1386                                .anchor(anchor)
1387                                .child(
1388                                    div()
1389                                        .id("submenu")
1390                                        .test_support()
1391                                        .occlude()
1392                                        .when(is_bottom_pos, |this| this.bottom_0())
1393                                        .when(!is_bottom_pos, |this| this.top_neg_1())
1394                                        .left(left)
1395                                        .child(menu.clone()),
1396                                )
1397                                .snap_to_window_with_margin(Edges::all(EDGE_PADDING)),
1398                        )
1399                        .with_priority(self.priority + 1)
1400                    })
1401                }),
1402        }
1403    }
1404}
1405
1406impl FluentBuilder for PopupMenu {}
1407impl EventEmitter<DismissEvent> for PopupMenu {}
1408impl Focusable for PopupMenu {
1409    fn focus_handle(&self, _: &App) -> FocusHandle {
1410        self.focus_handle.clone()
1411    }
1412}
1413
1414#[derive(Clone, Copy)]
1415struct RenderOptions {
1416    has_left_icon: bool,
1417    check_side: Side,
1418    radius: Pixels,
1419}
1420
1421impl Render for PopupMenu {
1422    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1423        self.update_submenu_menu_anchor(window);
1424
1425        // Submenus attached via the public `item()` + `PopupMenuItem::submenu()`
1426        // path (from contexts that only have the menu value, e.g. a table
1427        // delegate's `context_menu`) have no parent wired at construction time.
1428        // Wire them here so the dismiss chain, click-outside checks and keyboard
1429        // navigation treat them the same as `submenu()`-built children.
1430        let parent = cx.entity().downgrade();
1431        let parent_priority = self.priority;
1432        for item in &self.menu_items {
1433            if let PopupMenuItem::Submenu { menu, .. } = item {
1434                if menu.read(cx).parent_menu.is_none() {
1435                    menu.update(cx, |menu, _| {
1436                        menu.parent_menu = Some(parent.clone());
1437                        menu.priority = parent_priority + 1;
1438                    });
1439                }
1440            }
1441        }
1442
1443        let view = cx.entity().clone();
1444        let items_count = self.menu_items.len();
1445
1446        let max_height = self.max_height.unwrap_or_else(|| {
1447            let window_half_height = window.window_bounds().get_bounds().size.height * 0.5;
1448            window_half_height.min(px(450.))
1449        });
1450
1451        let has_left_icon = self
1452            .menu_items
1453            .iter()
1454            .any(|item| item.has_left_icon(self.check_side));
1455
1456        let max_width = self.max_width();
1457        let options = RenderOptions {
1458            has_left_icon,
1459            check_side: self.check_side,
1460            radius: cx.theme().radius.min(px(8.)),
1461        };
1462
1463        v_flex()
1464            .id("popup-menu")
1465            .test_support()
1466            .role(Role::Menu)
1467            .key_context(CONTEXT)
1468            .track_focus(&self.focus_handle)
1469            .on_action(cx.listener(Self::select_up))
1470            .on_action(cx.listener(Self::select_down))
1471            .on_action(cx.listener(Self::select_left))
1472            .on_action(cx.listener(Self::select_right))
1473            .on_action(cx.listener(Self::confirm))
1474            .on_action(cx.listener(Self::dismiss))
1475            .on_mouse_down_out(cx.listener(Self::on_mouse_down_out))
1476            .popover_style(cx)
1477            .text_color(cx.theme().popover_foreground)
1478            .relative()
1479            .occlude()
1480            .child(
1481                v_flex()
1482                    .id("items")
1483                    .p_1()
1484                    .gap_y_0p5()
1485                    .min_w(rems(8.))
1486                    .when_some(self.min_width, |this, min_width| this.min_w(min_width))
1487                    .max_w(max_width)
1488                    .when(self.scrollable, |this| {
1489                        this.max_h(max_height)
1490                            .overflow_y_scroll()
1491                            .track_scroll(&self.scroll_handle)
1492                    })
1493                    .children(
1494                        self.menu_items
1495                            .iter()
1496                            .enumerate()
1497                            // Ignore last separator
1498                            .filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
1499                            .map(|(ix, item)| self.render_item(ix, item, options, window, cx)),
1500                    )
1501                    .on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)),
1502            )
1503            .when(self.scrollable, |this| {
1504                this.vertical_scrollbar(&self.scroll_handle)
1505            })
1506    }
1507}
1508
1509#[cfg(test)]
1510mod tests {
1511    use super::*;
1512
1513    #[gpui::test]
1514    fn popup_menu_item_a11y_label_uses_visible_label(cx: &mut gpui::TestAppContext) {
1515        let submenu = cx.update(|cx| cx.new(|cx| PopupMenu::new(cx)));
1516
1517        assert_eq!(PopupMenuItem::new("Open").a11y_label(), Some("Open".into()));
1518        assert_eq!(
1519            PopupMenuItem::link("Docs", "https://example.com").a11y_label(),
1520            Some("Docs".into())
1521        );
1522        assert_eq!(
1523            PopupMenuItem::label("Recent files").a11y_label(),
1524            Some("Recent files".into())
1525        );
1526        assert_eq!(
1527            PopupMenuItem::submenu("More", submenu).a11y_label(),
1528            Some("More".into())
1529        );
1530        assert_eq!(PopupMenuItem::separator().a11y_label(), None);
1531        assert_eq!(PopupMenuItem::element(|_, _| div()).a11y_label(), None);
1532    }
1533}