gpui_component/menu/
popup_menu.rs

1use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
2use crate::actions::{SelectLeft, SelectRight};
3use crate::menu::menu_item::MenuItemElement;
4use crate::scroll::{Scrollbar, ScrollbarState};
5use crate::{h_flex, v_flex, ActiveTheme, Icon, IconName, Sizable as _};
6use crate::{kbd::Kbd, Side, Size, StyledExt};
7use gpui::{
8    anchored, canvas, div, prelude::FluentBuilder, px, rems, Action, AnyElement, App, AppContext,
9    Bounds, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable,
10    InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, ScrollHandle,
11    SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
12};
13use gpui::{ClickEvent, Half, MouseDownEvent, OwnedMenuItem, Subscription};
14use std::rc::Rc;
15
16const CONTEXT: &str = "PopupMenu";
17
18pub fn init(cx: &mut App) {
19    cx.bind_keys([
20        KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
21        KeyBinding::new("escape", Cancel, Some(CONTEXT)),
22        KeyBinding::new("up", SelectUp, Some(CONTEXT)),
23        KeyBinding::new("down", SelectDown, Some(CONTEXT)),
24        KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
25        KeyBinding::new("right", SelectRight, Some(CONTEXT)),
26    ]);
27}
28
29/// An menu item in a popup menu.
30pub enum PopupMenuItem {
31    Separator,
32    /// A non-interactive label item.
33    Label(SharedString),
34    /// A standard menu item.
35    Item {
36        icon: Option<Icon>,
37        label: SharedString,
38        disabled: bool,
39        is_link: bool,
40        action: Option<Box<dyn Action>>,
41        // For link item
42        handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
43    },
44    /// A menu item with custom element render.
45    ElementItem {
46        icon: Option<Icon>,
47        disabled: bool,
48        action: Option<Box<dyn Action>>,
49        render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
50        handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
51    },
52    /// A submenu item that opens another popup menu.
53    ///
54    /// NOTE: This is only supported when the parent menu is not `scrollable`.
55    Submenu {
56        icon: Option<Icon>,
57        label: SharedString,
58        disabled: bool,
59        menu: Entity<PopupMenu>,
60    },
61}
62
63impl FluentBuilder for PopupMenuItem {}
64impl PopupMenuItem {
65    /// Create a new menu item with the given label.
66    #[inline]
67    pub fn new(label: impl Into<SharedString>) -> Self {
68        PopupMenuItem::Item {
69            icon: None,
70            label: label.into(),
71            disabled: false,
72            action: None,
73            is_link: false,
74            handler: None,
75        }
76    }
77
78    /// Create a new menu item with custom element render.
79    #[inline]
80    pub fn element<F, E>(builder: F) -> Self
81    where
82        F: Fn(&mut Window, &mut App) -> E + 'static,
83        E: IntoElement,
84    {
85        PopupMenuItem::ElementItem {
86            icon: None,
87            disabled: false,
88            action: None,
89            render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
90            handler: None,
91        }
92    }
93
94    /// Create a new submenu item that opens another popup menu.
95    #[inline]
96    pub fn submenu(label: impl Into<SharedString>, menu: Entity<PopupMenu>) -> Self {
97        PopupMenuItem::Submenu {
98            icon: None,
99            label: label.into(),
100            disabled: false,
101            menu,
102        }
103    }
104
105    /// Create a separator menu item.
106    #[inline]
107    pub fn separator() -> Self {
108        PopupMenuItem::Separator
109    }
110
111    /// Creates a label menu item.
112    #[inline]
113    pub fn label(label: impl Into<SharedString>) -> Self {
114        PopupMenuItem::Label(label.into())
115    }
116
117    /// Set the icon for the menu item.
118    ///
119    /// Only works for [`PopupMenuItem::Item`], [`PopupMenuItem::ElementItem`] and [`PopupMenuItem::Submenu`].
120    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
121        match &mut self {
122            PopupMenuItem::Item { icon: i, .. } => {
123                *i = Some(icon.into());
124            }
125            PopupMenuItem::ElementItem { icon: i, .. } => {
126                *i = Some(icon.into());
127            }
128            PopupMenuItem::Submenu { icon: i, .. } => {
129                *i = Some(icon.into());
130            }
131            _ => {}
132        }
133        self
134    }
135
136    /// Set the action for the menu item.
137    ///
138    /// Only works for [`PopupMenuItem::Item`] and [`PopupMenuItem::ElementItem`].
139    pub fn action(mut self, action: Box<dyn Action>) -> Self {
140        match &mut self {
141            PopupMenuItem::Item { action: a, .. } => {
142                *a = Some(action);
143            }
144            PopupMenuItem::ElementItem { action: a, .. } => {
145                *a = Some(action);
146            }
147            _ => {}
148        }
149        self
150    }
151
152    /// Set the disabled state for the menu item.
153    ///
154    /// Only works for [`PopupMenuItem::Item`], [`PopupMenuItem::ElementItem`] and [`PopupMenuItem::Submenu`].
155    pub fn disabled(mut self, disabled: bool) -> Self {
156        match &mut self {
157            PopupMenuItem::Item { disabled: d, .. } => {
158                *d = disabled;
159            }
160            PopupMenuItem::ElementItem { disabled: d, .. } => {
161                *d = disabled;
162            }
163            PopupMenuItem::Submenu { disabled: d, .. } => {
164                *d = disabled;
165            }
166            _ => {}
167        }
168        self
169    }
170
171    /// Set checked state for the menu item by adding or removing check icon.
172    ///
173    /// If true, will set the icon to check icon, otherwise remove the icon.
174    pub fn checked(mut self, checked: bool) -> Self {
175        match &mut self {
176            PopupMenuItem::Item { icon: i, .. } => {
177                if checked {
178                    *i = Some(IconName::Check.into());
179                } else {
180                    *i = None;
181                }
182            }
183            PopupMenuItem::ElementItem { icon: i, .. } => {
184                if checked {
185                    *i = Some(IconName::Check.into());
186                } else {
187                    *i = None;
188                }
189            }
190            _ => {}
191        }
192        self
193    }
194
195    /// Add a click handler for the menu item.
196    ///
197    /// Only works for [`PopupMenuItem::Item`] and [`PopupMenuItem::ElementItem`].
198    pub fn on_click<F>(mut self, handler: F) -> Self
199    where
200        F: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
201    {
202        match &mut self {
203            PopupMenuItem::Item { handler: h, .. } => {
204                *h = Some(Rc::new(handler));
205            }
206            PopupMenuItem::ElementItem { handler: h, .. } => {
207                *h = Some(Rc::new(handler));
208            }
209            _ => {}
210        }
211        self
212    }
213
214    /// Create a link menu item.
215    #[inline]
216    pub fn link(label: impl Into<SharedString>, href: impl Into<String>) -> Self {
217        let href = href.into();
218        PopupMenuItem::Item {
219            icon: None,
220            label: label.into(),
221            disabled: false,
222            action: None,
223            is_link: true,
224            handler: Some(Rc::new(move |_, _, cx| cx.open_url(&href))),
225        }
226    }
227
228    #[inline]
229    fn is_clickable(&self) -> bool {
230        !matches!(self, PopupMenuItem::Separator)
231            && matches!(
232                self,
233                PopupMenuItem::Item {
234                    disabled: false,
235                    ..
236                } | PopupMenuItem::ElementItem {
237                    disabled: false,
238                    ..
239                } | PopupMenuItem::Submenu {
240                    disabled: false,
241                    ..
242                }
243            )
244    }
245
246    #[inline]
247    fn is_separator(&self) -> bool {
248        matches!(self, PopupMenuItem::Separator)
249    }
250
251    fn has_icon(&self) -> bool {
252        match self {
253            PopupMenuItem::Item { icon, .. } => icon.is_some(),
254            PopupMenuItem::ElementItem { icon, .. } => icon.is_some(),
255            PopupMenuItem::Submenu { icon, .. } => icon.is_some(),
256            _ => false,
257        }
258    }
259}
260
261pub struct PopupMenu {
262    pub(crate) focus_handle: FocusHandle,
263    pub(crate) menu_items: Vec<PopupMenuItem>,
264    /// The focus handle of Entity to handle actions.
265    pub(crate) action_context: Option<FocusHandle>,
266    has_icon: bool,
267    selected_index: Option<usize>,
268    min_width: Option<Pixels>,
269    max_width: Option<Pixels>,
270    max_height: Option<Pixels>,
271    bounds: Bounds<Pixels>,
272    size: Size,
273
274    /// The parent menu of this menu, if this is a submenu
275    parent_menu: Option<WeakEntity<Self>>,
276    scrollable: bool,
277    external_link_icon: bool,
278    scroll_handle: ScrollHandle,
279    scroll_state: ScrollbarState,
280    // This will update on render
281    submenu_anchor: (Corner, Pixels),
282
283    _subscriptions: Vec<Subscription>,
284}
285
286impl PopupMenu {
287    pub(crate) fn new(cx: &mut App) -> Self {
288        Self {
289            focus_handle: cx.focus_handle(),
290            action_context: None,
291            parent_menu: None,
292            menu_items: Vec::new(),
293            selected_index: None,
294            min_width: None,
295            max_width: None,
296            max_height: None,
297            has_icon: false,
298            bounds: Bounds::default(),
299            scrollable: false,
300            scroll_handle: ScrollHandle::default(),
301            scroll_state: ScrollbarState::default(),
302            external_link_icon: true,
303            size: Size::default(),
304            submenu_anchor: (Corner::TopLeft, Pixels::ZERO),
305            _subscriptions: vec![],
306        }
307    }
308
309    pub fn build(
310        window: &mut Window,
311        cx: &mut App,
312        f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
313    ) -> Entity<Self> {
314        cx.new(|cx| f(Self::new(cx), window, cx))
315    }
316
317    /// Set the focus handle of Entity to handle actions.
318    ///
319    /// When the menu is dismissed or before an action is triggered, the focus will be returned to this handle.
320    ///
321    /// Then the action will be dispatched to this handle.
322    pub fn action_context(mut self, handle: FocusHandle) -> Self {
323        self.action_context = Some(handle);
324        self
325    }
326
327    /// Set min width of the popup menu, default is 120px
328    pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
329        self.min_width = Some(width.into());
330        self
331    }
332
333    /// Set max width of the popup menu, default is 500px
334    pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
335        self.max_width = Some(width.into());
336        self
337    }
338
339    /// Set max height of the popup menu, default is half of the window height
340    pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
341        self.max_height = Some(height.into());
342        self
343    }
344
345    /// Set the menu to be scrollable to show vertical scrollbar.
346    ///
347    /// NOTE: If this is true, the sub-menus will cannot be support.
348    pub fn scrollable(mut self) -> Self {
349        self.scrollable = true;
350        self
351    }
352
353    /// Set the menu to show external link icon, default is true.
354    pub fn external_link_icon(mut self, visible: bool) -> Self {
355        self.external_link_icon = visible;
356        self
357    }
358
359    /// Add Menu Item
360    pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
361        self.menu_with_disabled(label, action, false)
362    }
363
364    /// Add Menu Item with enable state
365    pub fn menu_with_enable(
366        mut self,
367        label: impl Into<SharedString>,
368        action: Box<dyn Action>,
369        enable: bool,
370    ) -> Self {
371        self.add_menu_item(label, None, action, !enable);
372        self
373    }
374
375    /// Add Menu Item with disabled state
376    pub fn menu_with_disabled(
377        mut self,
378        label: impl Into<SharedString>,
379        action: Box<dyn Action>,
380        disabled: bool,
381    ) -> Self {
382        self.add_menu_item(label, None, action, disabled);
383        self
384    }
385
386    /// Add label
387    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
388        self.menu_items.push(PopupMenuItem::label(label.into()));
389        self
390    }
391
392    /// Add Menu to open link
393    pub fn link(self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
394        self.link_with_disabled(label, href, false)
395    }
396
397    /// Add Menu to open link with disabled state
398    pub fn link_with_disabled(
399        mut self,
400        label: impl Into<SharedString>,
401        href: impl Into<String>,
402        disabled: bool,
403    ) -> Self {
404        let href = href.into();
405        self.menu_items
406            .push(PopupMenuItem::link(label, href).disabled(disabled));
407        self
408    }
409
410    /// Add Menu to open link
411    pub fn link_with_icon(
412        self,
413        label: impl Into<SharedString>,
414        icon: impl Into<Icon>,
415        href: impl Into<String>,
416    ) -> Self {
417        self.link_with_icon_and_disabled(label, icon, href, false)
418    }
419
420    /// Add Menu to open link with icon and disabled state
421    fn link_with_icon_and_disabled(
422        mut self,
423        label: impl Into<SharedString>,
424        icon: impl Into<Icon>,
425        href: impl Into<String>,
426        disabled: bool,
427    ) -> Self {
428        let href = href.into();
429        self.menu_items.push(
430            PopupMenuItem::link(label, href)
431                .icon(icon)
432                .disabled(disabled),
433        );
434        self
435    }
436
437    /// Add Menu Item with Icon.
438    pub fn menu_with_icon(
439        self,
440        label: impl Into<SharedString>,
441        icon: impl Into<Icon>,
442        action: Box<dyn Action>,
443    ) -> Self {
444        self.menu_with_icon_and_disabled(label, icon, action, false)
445    }
446
447    /// Add Menu Item with Icon and disabled state
448    pub fn menu_with_icon_and_disabled(
449        mut self,
450        label: impl Into<SharedString>,
451        icon: impl Into<Icon>,
452        action: Box<dyn Action>,
453        disabled: bool,
454    ) -> Self {
455        self.add_menu_item(label, Some(icon.into()), action, disabled);
456        self
457    }
458
459    /// Add Menu Item with check icon
460    pub fn menu_with_check(
461        self,
462        label: impl Into<SharedString>,
463        checked: bool,
464        action: Box<dyn Action>,
465    ) -> Self {
466        self.menu_with_check_and_disabled(label, checked, action, false)
467    }
468
469    /// Add Menu Item with check icon and disabled state
470    pub fn menu_with_check_and_disabled(
471        mut self,
472        label: impl Into<SharedString>,
473        checked: bool,
474        action: Box<dyn Action>,
475        disabled: bool,
476    ) -> Self {
477        if checked {
478            self.add_menu_item(label, Some(IconName::Check.into()), action, disabled);
479        } else {
480            self.add_menu_item(label, None, action, disabled);
481        }
482
483        self
484    }
485
486    /// Add Menu Item with custom element render.
487    pub fn menu_element<F, E>(self, action: Box<dyn Action>, builder: F) -> Self
488    where
489        F: Fn(&mut Window, &mut App) -> E + 'static,
490        E: IntoElement,
491    {
492        self.menu_element_with_check(false, action, builder)
493    }
494
495    /// Add Menu Item with custom element render with disabled state.
496    pub fn menu_element_with_disabled<F, E>(
497        self,
498        action: Box<dyn Action>,
499        disabled: bool,
500        builder: F,
501    ) -> Self
502    where
503        F: Fn(&mut Window, &mut App) -> E + 'static,
504        E: IntoElement,
505    {
506        self.menu_element_with_check_and_disabled(false, action, disabled, builder)
507    }
508
509    /// Add Menu Item with custom element render with icon.
510    pub fn menu_element_with_icon<F, E>(
511        self,
512        icon: impl Into<Icon>,
513        action: Box<dyn Action>,
514        builder: F,
515    ) -> Self
516    where
517        F: Fn(&mut Window, &mut App) -> E + 'static,
518        E: IntoElement,
519    {
520        self.menu_element_with_icon_and_disabled(icon, action, false, builder)
521    }
522
523    /// Add Menu Item with custom element render with check state
524    pub fn menu_element_with_check<F, E>(
525        self,
526        checked: bool,
527        action: Box<dyn Action>,
528        builder: F,
529    ) -> Self
530    where
531        F: Fn(&mut Window, &mut App) -> E + 'static,
532        E: IntoElement,
533    {
534        self.menu_element_with_check_and_disabled(checked, action, false, builder)
535    }
536
537    /// Add Menu Item with custom element render with icon and disabled state
538    fn menu_element_with_icon_and_disabled<F, E>(
539        mut self,
540        icon: impl Into<Icon>,
541        action: Box<dyn Action>,
542        disabled: bool,
543        builder: F,
544    ) -> Self
545    where
546        F: Fn(&mut Window, &mut App) -> E + 'static,
547        E: IntoElement,
548    {
549        self.menu_items.push(
550            PopupMenuItem::element(builder)
551                .action(action)
552                .icon(icon)
553                .disabled(disabled),
554        );
555        self.has_icon = true;
556        self
557    }
558
559    /// Add Menu Item with custom element render with check state and disabled state
560    fn menu_element_with_check_and_disabled<F, E>(
561        mut self,
562        checked: bool,
563        action: Box<dyn Action>,
564        disabled: bool,
565        builder: F,
566    ) -> Self
567    where
568        F: Fn(&mut Window, &mut App) -> E + 'static,
569        E: IntoElement,
570    {
571        self.menu_items.push(
572            PopupMenuItem::element(builder)
573                .action(action)
574                .when(checked, |item| item.icon(IconName::Check))
575                .disabled(disabled),
576        );
577        self.has_icon = self.has_icon || checked;
578        self
579    }
580
581    /// Add a separator Menu Item
582    pub fn separator(mut self) -> Self {
583        if self.menu_items.is_empty() {
584            return self;
585        }
586
587        if let Some(PopupMenuItem::Separator) = self.menu_items.last() {
588            return self;
589        }
590
591        self.menu_items.push(PopupMenuItem::separator());
592        self
593    }
594
595    /// Add a Submenu
596    pub fn submenu(
597        self,
598        label: impl Into<SharedString>,
599        window: &mut Window,
600        cx: &mut Context<Self>,
601        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
602    ) -> Self {
603        self.submenu_with_icon(None, label, window, cx, f)
604    }
605
606    /// Add a Submenu item with icon
607    pub fn submenu_with_icon(
608        mut self,
609        icon: Option<Icon>,
610        label: impl Into<SharedString>,
611        window: &mut Window,
612        cx: &mut Context<Self>,
613        f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
614    ) -> Self {
615        let submenu = PopupMenu::build(window, cx, f);
616        let parent_menu = cx.entity().downgrade();
617        submenu.update(cx, |view, _| {
618            view.parent_menu = Some(parent_menu);
619        });
620
621        self.menu_items.push(
622            PopupMenuItem::submenu(label, submenu).when_some(icon, |this, icon| this.icon(icon)),
623        );
624        self
625    }
626
627    /// Add menu item.
628    pub fn item(mut self, item: impl Into<PopupMenuItem>) -> Self {
629        let item: PopupMenuItem = item.into();
630        if item.has_icon() {
631            self.has_icon = true;
632        }
633        self.menu_items.push(item);
634        self
635    }
636
637    /// Use small size, the menu item will have smaller height.
638    pub(crate) fn small(mut self) -> Self {
639        self.size = Size::Small;
640        self
641    }
642
643    fn add_menu_item(
644        &mut self,
645        label: impl Into<SharedString>,
646        icon: Option<Icon>,
647        action: Box<dyn Action>,
648        disabled: bool,
649    ) -> &mut Self {
650        if icon.is_some() {
651            self.has_icon = true;
652        }
653
654        self.menu_items.push(
655            PopupMenuItem::new(label)
656                .when_some(icon, |item, icon| item.icon(icon))
657                .disabled(disabled)
658                .action(action),
659        );
660        self
661    }
662
663    pub(super) fn with_menu_items<I>(
664        mut self,
665        items: impl IntoIterator<Item = I>,
666        window: &mut Window,
667        cx: &mut Context<Self>,
668    ) -> Self
669    where
670        I: Into<OwnedMenuItem>,
671    {
672        for item in items {
673            match item.into() {
674                OwnedMenuItem::Action { name, action, .. } => {
675                    self = self.menu(name, action.boxed_clone())
676                }
677                OwnedMenuItem::Separator => {
678                    self = self.separator();
679                }
680                OwnedMenuItem::Submenu(submenu) => {
681                    self = self.submenu(submenu.name, window, cx, move |menu, window, cx| {
682                        menu.with_menu_items(submenu.items.clone(), window, cx)
683                    })
684                }
685                OwnedMenuItem::SystemMenu(_) => {}
686            }
687        }
688
689        if self.menu_items.len() > 20 {
690            self.scrollable = true;
691        }
692
693        self
694    }
695
696    pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
697        if let Some(ix) = self.selected_index {
698            if let Some(item) = self.menu_items.get(ix) {
699                return match item {
700                    PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
701                    _ => None,
702                };
703            }
704        }
705
706        None
707    }
708
709    pub fn is_empty(&self) -> bool {
710        self.menu_items.is_empty()
711    }
712
713    fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
714        self.menu_items
715            .iter()
716            .enumerate()
717            .filter(|(_, item)| item.is_clickable())
718    }
719
720    fn on_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
721        cx.stop_propagation();
722        window.prevent_default();
723        self.selected_index = Some(ix);
724        self.confirm(&Confirm { secondary: false }, window, cx);
725    }
726
727    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
728        match self.selected_index {
729            Some(index) => {
730                let item = self.menu_items.get(index);
731                match item {
732                    Some(PopupMenuItem::Item {
733                        handler, action, ..
734                    }) => {
735                        if let Some(handler) = handler {
736                            handler(&ClickEvent::default(), window, cx);
737                        } else if let Some(action) = action.as_ref() {
738                            self.dispatch_confirm_action(action, window, cx);
739                        }
740
741                        self.dismiss(&Cancel, window, cx)
742                    }
743                    Some(PopupMenuItem::ElementItem {
744                        handler, action, ..
745                    }) => {
746                        if let Some(handler) = handler {
747                            handler(&ClickEvent::default(), window, cx);
748                        } else if let Some(action) = action.as_ref() {
749                            self.dispatch_confirm_action(action, window, cx);
750                        }
751                        self.dismiss(&Cancel, window, cx)
752                    }
753                    _ => {}
754                }
755            }
756            _ => {}
757        }
758    }
759
760    fn dispatch_confirm_action(
761        &self,
762        action: &Box<dyn Action>,
763        window: &mut Window,
764        cx: &mut Context<Self>,
765    ) {
766        if let Some(context) = self.action_context.as_ref() {
767            context.focus(window);
768        }
769
770        window.dispatch_action(action.boxed_clone(), cx);
771    }
772
773    fn set_selected_index(&mut self, ix: usize, cx: &mut Context<Self>) {
774        if self.selected_index != Some(ix) {
775            self.selected_index = Some(ix);
776            self.scroll_handle.scroll_to_item(ix);
777            cx.notify();
778        }
779    }
780
781    fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
782        cx.stop_propagation();
783        let ix = self.selected_index.unwrap_or(0);
784
785        if let Some((prev_ix, _)) = self
786            .menu_items
787            .iter()
788            .enumerate()
789            .rev()
790            .find(|(i, item)| *i < ix && item.is_clickable())
791        {
792            self.set_selected_index(prev_ix, cx);
793            return;
794        }
795
796        let last_clickable_ix = self.clickable_menu_items().last().map(|(ix, _)| ix);
797        self.set_selected_index(last_clickable_ix.unwrap_or(0), cx);
798    }
799
800    fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
801        cx.stop_propagation();
802        let Some(ix) = self.selected_index else {
803            self.set_selected_index(0, cx);
804            return;
805        };
806
807        if let Some((next_ix, _)) = self
808            .menu_items
809            .iter()
810            .enumerate()
811            .find(|(i, item)| *i > ix && item.is_clickable())
812        {
813            self.set_selected_index(next_ix, cx);
814            return;
815        }
816
817        self.set_selected_index(0, cx);
818    }
819
820    fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
821        let handled = if matches!(self.submenu_anchor.0, Corner::TopLeft | Corner::BottomLeft) {
822            self._unselect_submenu(window, cx)
823        } else {
824            self._select_submenu(window, cx)
825        };
826
827        if self.parent_side(cx).is_left() {
828            self._focus_parent_menu(window, cx);
829        }
830
831        if handled {
832            return;
833        }
834
835        // For parent AppMenuBar to handle.
836        if self.parent_menu.is_none() {
837            cx.propagate();
838        }
839    }
840
841    fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
842        let handled = if matches!(self.submenu_anchor.0, Corner::TopLeft | Corner::BottomLeft) {
843            self._select_submenu(window, cx)
844        } else {
845            self._unselect_submenu(window, cx)
846        };
847
848        if self.parent_side(cx).is_right() {
849            self._focus_parent_menu(window, cx);
850        }
851
852        if handled {
853            return;
854        }
855
856        // For parent AppMenuBar to handle.
857        if self.parent_menu.is_none() {
858            cx.propagate();
859        }
860    }
861
862    fn _select_submenu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
863        if let Some(active_submenu) = self.active_submenu() {
864            // Focus the submenu, so that can be handle the action.
865            active_submenu.update(cx, |view, cx| {
866                view.set_selected_index(0, cx);
867                view.focus_handle.focus(window);
868            });
869            cx.notify();
870            return true;
871        }
872
873        return false;
874    }
875
876    fn _unselect_submenu(&mut self, _: &mut Window, cx: &mut Context<Self>) -> bool {
877        if let Some(active_submenu) = self.active_submenu() {
878            active_submenu.update(cx, |view, cx| {
879                view.selected_index = None;
880                cx.notify();
881            });
882            return true;
883        }
884
885        return false;
886    }
887
888    fn _focus_parent_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
889        let Some(parent) = self.parent_menu.as_ref() else {
890            return;
891        };
892        let Some(parent) = parent.upgrade() else {
893            return;
894        };
895
896        self.selected_index = None;
897        parent.update(cx, |view, cx| {
898            view.focus_handle.focus(window);
899            cx.notify();
900        });
901    }
902
903    fn parent_side(&self, cx: &App) -> Side {
904        let Some(parent) = self.parent_menu.as_ref() else {
905            return Side::Left;
906        };
907
908        let Some(parent) = parent.upgrade() else {
909            return Side::Left;
910        };
911
912        match parent.read(cx).submenu_anchor.0 {
913            Corner::TopLeft | Corner::BottomLeft => Side::Left,
914            Corner::TopRight | Corner::BottomRight => Side::Right,
915        }
916    }
917
918    fn dismiss(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
919        if self.active_submenu().is_some() {
920            return;
921        }
922
923        cx.emit(DismissEvent);
924
925        // Focus back to the previous focused handle.
926        if let Some(action_context) = self.action_context.as_ref() {
927            window.focus(action_context);
928        }
929
930        let Some(parent_menu) = self.parent_menu.clone() else {
931            return;
932        };
933
934        // Dismiss parent menu, when this menu is dismissed
935        _ = parent_menu.update(cx, |view, cx| {
936            view.selected_index = None;
937            view.dismiss(&Cancel, window, cx);
938        });
939    }
940
941    fn render_key_binding(
942        &self,
943        action: Option<Box<dyn Action>>,
944        window: &mut Window,
945        _: &mut Context<Self>,
946    ) -> Option<impl IntoElement> {
947        let action = action?;
948
949        match self
950            .action_context
951            .as_ref()
952            .and_then(|handle| Kbd::binding_for_action_in(action.as_ref(), handle, window))
953        {
954            Some(kbd) => Some(kbd),
955            // Fallback to App level key binding
956            None => Kbd::binding_for_action(action.as_ref(), None, window),
957        }
958        .map(|this| {
959            this.p_0()
960                .flex_nowrap()
961                .border_0()
962                .bg(gpui::transparent_white())
963        })
964    }
965
966    fn render_icon(
967        has_icon: bool,
968        icon: Option<Icon>,
969        _: &mut Window,
970        _: &mut Context<Self>,
971    ) -> Option<impl IntoElement> {
972        let icon_placeholder = if has_icon { Some(Icon::empty()) } else { None };
973
974        if !has_icon {
975            return None;
976        }
977
978        let icon = h_flex()
979            .w_3p5()
980            .h_3p5()
981            .justify_center()
982            .text_sm()
983            .map(|this| {
984                if let Some(icon) = icon {
985                    this.child(icon.clone().xsmall())
986                } else {
987                    this.children(icon_placeholder.clone())
988                }
989            });
990
991        Some(icon)
992    }
993
994    #[inline]
995    fn max_width(&self) -> Pixels {
996        self.max_width.unwrap_or(px(500.))
997    }
998
999    /// Calculate the anchor corner and left offset for child submenu
1000    fn update_submenu_menu_anchor(&mut self, window: &Window) {
1001        let bounds = self.bounds;
1002        let max_width = self.max_width();
1003        let (anchor, left) = if max_width + bounds.origin.x > window.bounds().size.width {
1004            (Corner::TopRight, -px(16.))
1005        } else {
1006            (Corner::TopLeft, bounds.size.width - px(8.))
1007        };
1008
1009        let is_bottom_pos = bounds.origin.y + bounds.size.height > window.bounds().size.height;
1010        self.submenu_anchor = if is_bottom_pos {
1011            (anchor.other_side_corner_along(gpui::Axis::Vertical), left)
1012        } else {
1013            (anchor, left)
1014        };
1015    }
1016
1017    fn render_item(
1018        &self,
1019        ix: usize,
1020        item: &PopupMenuItem,
1021        state: ItemState,
1022        window: &mut Window,
1023        cx: &mut Context<Self>,
1024    ) -> impl IntoElement {
1025        let has_icon = self.has_icon;
1026        let selected = self.selected_index == Some(ix);
1027        const EDGE_PADDING: Pixels = px(4.);
1028        const INNER_PADDING: Pixels = px(8.);
1029
1030        let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
1031        let group_name = format!("popup-menu-item-{}", ix);
1032
1033        let (item_height, radius) = match self.size {
1034            Size::Small => (px(20.), state.radius.half()),
1035            _ => (px(26.), state.radius),
1036        };
1037
1038        let this = MenuItemElement::new(ix, &group_name)
1039            .relative()
1040            .text_sm()
1041            .py_0()
1042            .px(INNER_PADDING)
1043            .rounded(radius)
1044            .items_center()
1045            .selected(selected)
1046            .on_hover(cx.listener(move |this, hovered, _, cx| {
1047                if *hovered {
1048                    this.selected_index = Some(ix);
1049                } else if !is_submenu && this.selected_index == Some(ix) {
1050                    // TODO: Better handle the submenu unselection when hover out
1051                    this.selected_index = None;
1052                }
1053
1054                cx.notify();
1055            }));
1056
1057        match item {
1058            PopupMenuItem::Separator => this
1059                .h_auto()
1060                .p_0()
1061                .my_0p5()
1062                .mx_neg_1()
1063                .h(px(1.))
1064                .bg(cx.theme().border)
1065                .disabled(true),
1066            PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
1067                h_flex()
1068                    .cursor_default()
1069                    .items_center()
1070                    .gap_x_1()
1071                    .children(Self::render_icon(has_icon, None, window, cx))
1072                    .child(label.clone()),
1073            ),
1074            PopupMenuItem::ElementItem {
1075                render,
1076                icon,
1077                disabled,
1078                ..
1079            } => this
1080                .when(!disabled, |this| {
1081                    this.on_click(
1082                        cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
1083                    )
1084                })
1085                .disabled(*disabled)
1086                .child(
1087                    h_flex()
1088                        .flex_1()
1089                        .min_h(item_height)
1090                        .items_center()
1091                        .gap_x_1()
1092                        .children(Self::render_icon(has_icon, icon.clone(), window, cx))
1093                        .child((render)(window, cx)),
1094                ),
1095            PopupMenuItem::Item {
1096                icon,
1097                label,
1098                action,
1099                disabled,
1100                is_link,
1101                ..
1102            } => {
1103                let show_link_icon = *is_link && self.external_link_icon;
1104                let action = action.as_ref().map(|action| action.boxed_clone());
1105                let key = self.render_key_binding(action, window, cx);
1106
1107                this.when(!disabled, |this| {
1108                    this.on_click(
1109                        cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
1110                    )
1111                })
1112                .disabled(*disabled)
1113                .h(item_height)
1114                .children(Self::render_icon(has_icon, icon.clone(), window, cx))
1115                .child(
1116                    h_flex()
1117                        .w_full()
1118                        .gap_2()
1119                        .items_center()
1120                        .justify_between()
1121                        .when(!show_link_icon, |this| this.child(label.clone()))
1122                        .when(show_link_icon, |this| {
1123                            this.child(
1124                                h_flex()
1125                                    .w_full()
1126                                    .justify_between()
1127                                    .gap_1p5()
1128                                    .child(label.clone())
1129                                    .child(
1130                                        Icon::new(IconName::ExternalLink)
1131                                            .xsmall()
1132                                            .text_color(cx.theme().muted_foreground),
1133                                    ),
1134                            )
1135                        })
1136                        .children(key),
1137                )
1138            }
1139            PopupMenuItem::Submenu {
1140                icon,
1141                label,
1142                menu,
1143                disabled,
1144            } => this
1145                .selected(selected)
1146                .disabled(*disabled)
1147                .items_start()
1148                .child(
1149                    h_flex()
1150                        .min_h(item_height)
1151                        .size_full()
1152                        .items_center()
1153                        .gap_x_1()
1154                        .children(Self::render_icon(has_icon, icon.clone(), window, cx))
1155                        .child(
1156                            h_flex()
1157                                .flex_1()
1158                                .gap_2()
1159                                .items_center()
1160                                .justify_between()
1161                                .child(label.clone())
1162                                .child(IconName::ChevronRight),
1163                        ),
1164                )
1165                .when(selected, |this| {
1166                    this.child({
1167                        let (anchor, left) = self.submenu_anchor;
1168                        let is_bottom_pos =
1169                            matches!(anchor, Corner::BottomLeft | Corner::BottomRight);
1170                        anchored()
1171                            .anchor(anchor)
1172                            .child(
1173                                div()
1174                                    .id("submenu")
1175                                    .occlude()
1176                                    .when(is_bottom_pos, |this| this.bottom_0())
1177                                    .when(!is_bottom_pos, |this| this.top_neg_1())
1178                                    .left(left)
1179                                    .child(menu.clone()),
1180                            )
1181                            .snap_to_window_with_margin(Edges::all(EDGE_PADDING))
1182                    })
1183                }),
1184        }
1185    }
1186}
1187
1188impl FluentBuilder for PopupMenu {}
1189impl EventEmitter<DismissEvent> for PopupMenu {}
1190impl Focusable for PopupMenu {
1191    fn focus_handle(&self, _: &App) -> FocusHandle {
1192        self.focus_handle.clone()
1193    }
1194}
1195
1196#[derive(Clone, Copy)]
1197struct ItemState {
1198    radius: Pixels,
1199}
1200
1201impl Render for PopupMenu {
1202    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1203        self.update_submenu_menu_anchor(window);
1204
1205        let view = cx.entity().clone();
1206        let items_count = self.menu_items.len();
1207
1208        let max_height = self.max_height.map_or_else(
1209            || {
1210                let window_half_height = window.window_bounds().get_bounds().size.height * 0.5;
1211                window_half_height.min(px(450.))
1212            },
1213            |height| height,
1214        );
1215
1216        let max_width = self.max_width();
1217        let item_state = ItemState {
1218            radius: cx.theme().radius.min(px(8.)),
1219        };
1220
1221        v_flex()
1222            .id("popup-menu")
1223            .key_context(CONTEXT)
1224            .track_focus(&self.focus_handle)
1225            .on_action(cx.listener(Self::select_up))
1226            .on_action(cx.listener(Self::select_down))
1227            .on_action(cx.listener(Self::select_left))
1228            .on_action(cx.listener(Self::select_right))
1229            .on_action(cx.listener(Self::confirm))
1230            .on_action(cx.listener(Self::dismiss))
1231            .on_mouse_down_out(cx.listener(|this, ev: &MouseDownEvent, window, cx| {
1232                // Do not dismiss, if click inside the parent menu
1233                if let Some(parent) = this.parent_menu.as_ref() {
1234                    if let Some(parent) = parent.upgrade() {
1235                        if parent.read(cx).bounds.contains(&ev.position) {
1236                            return;
1237                        }
1238                    }
1239                }
1240
1241                this.dismiss(&Cancel, window, cx);
1242            }))
1243            .popover_style(cx)
1244            .text_color(cx.theme().popover_foreground)
1245            .relative()
1246            .child(
1247                v_flex()
1248                    .id("items")
1249                    .p_1()
1250                    .gap_y_0p5()
1251                    .min_w(rems(8.))
1252                    .when_some(self.min_width, |this, min_width| this.min_w(min_width))
1253                    .max_w(max_width)
1254                    .when(self.scrollable, |this| {
1255                        this.max_h(max_height)
1256                            .overflow_y_scroll()
1257                            .track_scroll(&self.scroll_handle)
1258                    })
1259                    .children(
1260                        self.menu_items
1261                            .iter()
1262                            .enumerate()
1263                            // Ignore last separator
1264                            .filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
1265                            .map(|(ix, item)| self.render_item(ix, item, item_state, window, cx)),
1266                    )
1267                    .child({
1268                        canvas(
1269                            move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
1270                            |_, _, _, _| {},
1271                        )
1272                        .absolute()
1273                        .size_full()
1274                    }),
1275            )
1276            .when(self.scrollable, |this| {
1277                // TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
1278                this.child(
1279                    div()
1280                        .absolute()
1281                        .top_0()
1282                        .left_0()
1283                        .right_0()
1284                        .bottom_0()
1285                        .child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)),
1286                )
1287            })
1288    }
1289}