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