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        if !has_icon {
974            return None;
975        }
976
977        let icon = if let Some(icon) = icon {
978            icon.clone()
979        } else {
980            Icon::empty()
981        };
982
983        Some(icon.xsmall())
984    }
985
986    #[inline]
987    fn max_width(&self) -> Pixels {
988        self.max_width.unwrap_or(px(500.))
989    }
990
991    /// Calculate the anchor corner and left offset for child submenu
992    fn update_submenu_menu_anchor(&mut self, window: &Window) {
993        let bounds = self.bounds;
994        let max_width = self.max_width();
995        let (anchor, left) = if max_width + bounds.origin.x > window.bounds().size.width {
996            (Corner::TopRight, -px(16.))
997        } else {
998            (Corner::TopLeft, bounds.size.width - px(8.))
999        };
1000
1001        let is_bottom_pos = bounds.origin.y + bounds.size.height > window.bounds().size.height;
1002        self.submenu_anchor = if is_bottom_pos {
1003            (anchor.other_side_corner_along(gpui::Axis::Vertical), left)
1004        } else {
1005            (anchor, left)
1006        };
1007    }
1008
1009    fn render_item(
1010        &self,
1011        ix: usize,
1012        item: &PopupMenuItem,
1013        state: ItemState,
1014        window: &mut Window,
1015        cx: &mut Context<Self>,
1016    ) -> impl IntoElement {
1017        let has_icon = self.has_icon;
1018        let selected = self.selected_index == Some(ix);
1019        const EDGE_PADDING: Pixels = px(4.);
1020        const INNER_PADDING: Pixels = px(8.);
1021
1022        let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
1023        let group_name = format!("popup-menu-item-{}", ix);
1024
1025        let (item_height, radius) = match self.size {
1026            Size::Small => (px(20.), state.radius.half()),
1027            _ => (px(26.), state.radius),
1028        };
1029
1030        let this = MenuItemElement::new(ix, &group_name)
1031            .relative()
1032            .text_sm()
1033            .py_0()
1034            .px(INNER_PADDING)
1035            .rounded(radius)
1036            .items_center()
1037            .selected(selected)
1038            .on_hover(cx.listener(move |this, hovered, _, cx| {
1039                if *hovered {
1040                    this.selected_index = Some(ix);
1041                } else if !is_submenu && this.selected_index == Some(ix) {
1042                    // TODO: Better handle the submenu unselection when hover out
1043                    this.selected_index = None;
1044                }
1045
1046                cx.notify();
1047            }));
1048
1049        match item {
1050            PopupMenuItem::Separator => this
1051                .h_auto()
1052                .p_0()
1053                .my_0p5()
1054                .mx_neg_1()
1055                .h(px(1.))
1056                .bg(cx.theme().border)
1057                .disabled(true),
1058            PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
1059                h_flex()
1060                    .cursor_default()
1061                    .items_center()
1062                    .gap_x_1()
1063                    .children(Self::render_icon(has_icon, None, window, cx))
1064                    .child(div().flex_1().child(label.clone())),
1065            ),
1066            PopupMenuItem::ElementItem {
1067                render,
1068                icon,
1069                disabled,
1070                ..
1071            } => this
1072                .when(!disabled, |this| {
1073                    this.on_click(
1074                        cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
1075                    )
1076                })
1077                .disabled(*disabled)
1078                .child(
1079                    h_flex()
1080                        .flex_1()
1081                        .min_h(item_height)
1082                        .items_center()
1083                        .gap_x_1()
1084                        .children(Self::render_icon(has_icon, icon.clone(), window, cx))
1085                        .child((render)(window, cx)),
1086                ),
1087            PopupMenuItem::Item {
1088                icon,
1089                label,
1090                action,
1091                disabled,
1092                is_link,
1093                ..
1094            } => {
1095                let show_link_icon = *is_link && self.external_link_icon;
1096                let action = action.as_ref().map(|action| action.boxed_clone());
1097                let key = self.render_key_binding(action, window, cx);
1098
1099                this.when(!disabled, |this| {
1100                    this.on_click(
1101                        cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
1102                    )
1103                })
1104                .disabled(*disabled)
1105                .h(item_height)
1106                .children(Self::render_icon(has_icon, icon.clone(), window, cx))
1107                .child(
1108                    h_flex()
1109                        .w_full()
1110                        .gap_2()
1111                        .items_center()
1112                        .justify_between()
1113                        .when(!show_link_icon, |this| this.child(label.clone()))
1114                        .when(show_link_icon, |this| {
1115                            this.child(
1116                                h_flex()
1117                                    .w_full()
1118                                    .justify_between()
1119                                    .gap_1p5()
1120                                    .child(label.clone())
1121                                    .child(
1122                                        Icon::new(IconName::ExternalLink)
1123                                            .xsmall()
1124                                            .text_color(cx.theme().muted_foreground),
1125                                    ),
1126                            )
1127                        })
1128                        .children(key),
1129                )
1130            }
1131            PopupMenuItem::Submenu {
1132                icon,
1133                label,
1134                menu,
1135                disabled,
1136            } => this
1137                .selected(selected)
1138                .disabled(*disabled)
1139                .items_start()
1140                .child(
1141                    h_flex()
1142                        .min_h(item_height)
1143                        .size_full()
1144                        .items_center()
1145                        .gap_x_1()
1146                        .children(Self::render_icon(has_icon, icon.clone(), window, cx))
1147                        .child(
1148                            h_flex()
1149                                .flex_1()
1150                                .gap_2()
1151                                .items_center()
1152                                .justify_between()
1153                                .child(label.clone())
1154                                .child(IconName::ChevronRight),
1155                        ),
1156                )
1157                .when(selected, |this| {
1158                    this.child({
1159                        let (anchor, left) = self.submenu_anchor;
1160                        let is_bottom_pos =
1161                            matches!(anchor, Corner::BottomLeft | Corner::BottomRight);
1162                        anchored()
1163                            .anchor(anchor)
1164                            .child(
1165                                div()
1166                                    .id("submenu")
1167                                    .occlude()
1168                                    .when(is_bottom_pos, |this| this.bottom_0())
1169                                    .when(!is_bottom_pos, |this| this.top_neg_1())
1170                                    .left(left)
1171                                    .child(menu.clone()),
1172                            )
1173                            .snap_to_window_with_margin(Edges::all(EDGE_PADDING))
1174                    })
1175                }),
1176        }
1177    }
1178}
1179
1180impl FluentBuilder for PopupMenu {}
1181impl EventEmitter<DismissEvent> for PopupMenu {}
1182impl Focusable for PopupMenu {
1183    fn focus_handle(&self, _: &App) -> FocusHandle {
1184        self.focus_handle.clone()
1185    }
1186}
1187
1188#[derive(Clone, Copy)]
1189struct ItemState {
1190    radius: Pixels,
1191}
1192
1193impl Render for PopupMenu {
1194    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1195        self.update_submenu_menu_anchor(window);
1196
1197        let view = cx.entity().clone();
1198        let items_count = self.menu_items.len();
1199
1200        let max_height = self.max_height.map_or_else(
1201            || {
1202                let window_half_height = window.window_bounds().get_bounds().size.height * 0.5;
1203                window_half_height.min(px(450.))
1204            },
1205            |height| height,
1206        );
1207
1208        let max_width = self.max_width();
1209        let item_state = ItemState {
1210            radius: cx.theme().radius.min(px(8.)),
1211        };
1212
1213        v_flex()
1214            .id("popup-menu")
1215            .key_context(CONTEXT)
1216            .track_focus(&self.focus_handle)
1217            .on_action(cx.listener(Self::select_up))
1218            .on_action(cx.listener(Self::select_down))
1219            .on_action(cx.listener(Self::select_left))
1220            .on_action(cx.listener(Self::select_right))
1221            .on_action(cx.listener(Self::confirm))
1222            .on_action(cx.listener(Self::dismiss))
1223            .on_mouse_down_out(cx.listener(|this, ev: &MouseDownEvent, window, cx| {
1224                // Do not dismiss, if click inside the parent menu
1225                if let Some(parent) = this.parent_menu.as_ref() {
1226                    if let Some(parent) = parent.upgrade() {
1227                        if parent.read(cx).bounds.contains(&ev.position) {
1228                            return;
1229                        }
1230                    }
1231                }
1232
1233                this.dismiss(&Cancel, window, cx);
1234            }))
1235            .popover_style(cx)
1236            .text_color(cx.theme().popover_foreground)
1237            .relative()
1238            .child(
1239                v_flex()
1240                    .id("items")
1241                    .p_1()
1242                    .gap_y_0p5()
1243                    .min_w(rems(8.))
1244                    .when_some(self.min_width, |this, min_width| this.min_w(min_width))
1245                    .max_w(max_width)
1246                    .when(self.scrollable, |this| {
1247                        this.max_h(max_height)
1248                            .overflow_y_scroll()
1249                            .track_scroll(&self.scroll_handle)
1250                    })
1251                    .children(
1252                        self.menu_items
1253                            .iter()
1254                            .enumerate()
1255                            // Ignore last separator
1256                            .filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
1257                            .map(|(ix, item)| self.render_item(ix, item, item_state, window, cx)),
1258                    )
1259                    .child({
1260                        canvas(
1261                            move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
1262                            |_, _, _, _| {},
1263                        )
1264                        .absolute()
1265                        .size_full()
1266                    }),
1267            )
1268            .when(self.scrollable, |this| {
1269                // TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
1270                this.child(
1271                    div()
1272                        .absolute()
1273                        .top_0()
1274                        .left_0()
1275                        .right_0()
1276                        .bottom_0()
1277                        .child(Scrollbar::vertical(&self.scroll_state, &self.scroll_handle)),
1278                )
1279            })
1280    }
1281}