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