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