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
29pub enum PopupMenuItem {
31 Separator,
33 Label(SharedString),
35 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 handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
45 },
46 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 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 #[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 #[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 #[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 #[inline]
112 pub fn separator() -> Self {
113 PopupMenuItem::Separator
114 }
115
116 #[inline]
118 pub fn label(label: impl Into<SharedString>) -> Self {
119 PopupMenuItem::Label(label.into())
120 }
121
122 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 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 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 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 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 #[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 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 parent_menu: Option<WeakEntity<Self>>,
287 scrollable: bool,
288 external_link_icon: bool,
289 scroll_handle: ScrollHandle,
290 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 pub fn action_context(mut self, handle: FocusHandle) -> Self {
332 self.action_context = Some(handle);
333 self
334 }
335
336 pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
338 self.min_width = Some(width.into());
339 self
340 }
341
342 pub fn max_w(mut self, width: impl Into<Pixels>) -> Self {
344 self.max_width = Some(width.into());
345 self
346 }
347
348 pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
350 self.max_height = Some(height.into());
351 self
352 }
353
354 pub fn scrollable(mut self, scrollable: bool) -> Self {
358 self.scrollable = scrollable;
359 self
360 }
361
362 pub fn check_side(mut self, side: Side) -> Self {
364 self.check_side = side;
365 self
366 }
367
368 pub fn external_link_icon(mut self, visible: bool) -> Self {
370 self.external_link_icon = visible;
371 self
372 }
373
374 pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
376 self.menu_with_disabled(label, action, false)
377 }
378
379 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 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 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
403 self.menu_items.push(PopupMenuItem::label(label.into()));
404 self
405 }
406
407 pub fn link(self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
409 self.link_with_disabled(label, href, false)
410 }
411
412 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 _ = 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 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 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 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 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 .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 this.vertical_scrollbar(&self.scroll_handle)
1310 })
1311 }
1312}