Skip to main content

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