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