1use crate::{
2 Icon, IconButtonShape, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator,
3 ListSubHeader, h_flex, prelude::*, utils::WithRemSize, v_flex,
4};
5use gpui::{
6 Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle,
7 Focusable, IntoElement, Render, Subscription, px,
8};
9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
10use settings::Settings;
11use std::{rc::Rc, time::Duration};
12use theme::ThemeSettings;
13
14use super::Tooltip;
15
16pub enum ContextMenuItem {
17 Separator,
18 Header(SharedString),
19 /// title, link_label, link_url
20 HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header
21 Label(SharedString),
22 Entry(ContextMenuEntry),
23 CustomEntry {
24 entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
25 handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
26 selectable: bool,
27 },
28}
29
30impl ContextMenuItem {
31 pub fn custom_entry(
32 entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
33 handler: impl Fn(&mut Window, &mut App) + 'static,
34 ) -> Self {
35 Self::CustomEntry {
36 entry_render: Box::new(entry_render),
37 handler: Rc::new(move |_, window, cx| handler(window, cx)),
38 selectable: true,
39 }
40 }
41}
42
43pub struct ContextMenuEntry {
44 toggle: Option<(IconPosition, bool)>,
45 label: SharedString,
46 icon: Option<IconName>,
47 icon_position: IconPosition,
48 icon_size: IconSize,
49 icon_color: Option<Color>,
50 handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
51 action: Option<Box<dyn Action>>,
52 disabled: bool,
53 documentation_aside: Option<DocumentationAside>,
54 end_slot_icon: Option<IconName>,
55 end_slot_title: Option<SharedString>,
56 end_slot_handler: Option<Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>>,
57 show_end_slot_on_hover: bool,
58}
59
60impl ContextMenuEntry {
61 pub fn new(label: impl Into<SharedString>) -> Self {
62 ContextMenuEntry {
63 toggle: None,
64 label: label.into(),
65 icon: None,
66 icon_position: IconPosition::Start,
67 icon_size: IconSize::Small,
68 icon_color: None,
69 handler: Rc::new(|_, _, _| {}),
70 action: None,
71 disabled: false,
72 documentation_aside: None,
73 end_slot_icon: None,
74 end_slot_title: None,
75 end_slot_handler: None,
76 show_end_slot_on_hover: false,
77 }
78 }
79
80 pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
81 self.toggle = Some((toggle_position, toggled));
82 self
83 }
84
85 pub fn icon(mut self, icon: IconName) -> Self {
86 self.icon = Some(icon);
87 self
88 }
89
90 pub fn icon_position(mut self, position: IconPosition) -> Self {
91 self.icon_position = position;
92 self
93 }
94
95 pub fn icon_size(mut self, icon_size: IconSize) -> Self {
96 self.icon_size = icon_size;
97 self
98 }
99
100 pub fn icon_color(mut self, icon_color: Color) -> Self {
101 self.icon_color = Some(icon_color);
102 self
103 }
104
105 pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
106 self.toggle = Some((toggle_position, toggled));
107 self
108 }
109
110 pub fn action(mut self, action: Box<dyn Action>) -> Self {
111 self.action = Some(action);
112 self
113 }
114
115 pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
116 self.handler = Rc::new(move |_, window, cx| handler(window, cx));
117 self
118 }
119
120 pub fn disabled(mut self, disabled: bool) -> Self {
121 self.disabled = disabled;
122 self
123 }
124
125 pub fn documentation_aside(
126 mut self,
127 side: DocumentationSide,
128 render: impl Fn(&mut App) -> AnyElement + 'static,
129 ) -> Self {
130 self.documentation_aside = Some(DocumentationAside {
131 side,
132 render: Rc::new(render),
133 });
134
135 self
136 }
137}
138
139impl From<ContextMenuEntry> for ContextMenuItem {
140 fn from(entry: ContextMenuEntry) -> Self {
141 ContextMenuItem::Entry(entry)
142 }
143}
144
145pub struct ContextMenu {
146 builder: Option<Rc<dyn Fn(Self, &mut Window, &mut Context<Self>) -> Self>>,
147 items: Vec<ContextMenuItem>,
148 focus_handle: FocusHandle,
149 action_context: Option<FocusHandle>,
150 selected_index: Option<usize>,
151 delayed: bool,
152 clicked: bool,
153 end_slot_action: Option<Box<dyn Action>>,
154 key_context: SharedString,
155 _on_blur_subscription: Subscription,
156 keep_open_on_confirm: bool,
157 eager: bool,
158 documentation_aside: Option<(usize, DocumentationAside)>,
159 fixed_width: Option<DefiniteLength>,
160}
161
162#[derive(Copy, Clone, PartialEq, Eq)]
163pub enum DocumentationSide {
164 Left,
165 Right,
166}
167
168#[derive(Clone)]
169pub struct DocumentationAside {
170 side: DocumentationSide,
171 render: Rc<dyn Fn(&mut App) -> AnyElement>,
172}
173
174impl Focusable for ContextMenu {
175 fn focus_handle(&self, _cx: &App) -> FocusHandle {
176 self.focus_handle.clone()
177 }
178}
179
180impl EventEmitter<DismissEvent> for ContextMenu {}
181
182impl FluentBuilder for ContextMenu {}
183
184impl ContextMenu {
185 pub fn build(
186 window: &mut Window,
187 cx: &mut App,
188 f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
189 ) -> Entity<Self> {
190 cx.new(|cx| {
191 let focus_handle = cx.focus_handle();
192 let _on_blur_subscription = cx.on_blur(
193 &focus_handle,
194 window,
195 |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
196 );
197 window.refresh();
198 f(
199 Self {
200 builder: None,
201 items: Default::default(),
202 focus_handle,
203 action_context: None,
204 selected_index: None,
205 delayed: false,
206 clicked: false,
207 key_context: "menu".into(),
208 _on_blur_subscription,
209 keep_open_on_confirm: false,
210 eager: false,
211 documentation_aside: None,
212 fixed_width: None,
213 end_slot_action: None,
214 },
215 window,
216 cx,
217 )
218 })
219 }
220
221 /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation.
222 ///
223 /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto
224 /// it to call it again.
225 pub fn build_persistent(
226 window: &mut Window,
227 cx: &mut App,
228 builder: impl Fn(Self, &mut Window, &mut Context<Self>) -> Self + 'static,
229 ) -> Entity<Self> {
230 cx.new(|cx| {
231 let builder = Rc::new(builder);
232
233 let focus_handle = cx.focus_handle();
234 let _on_blur_subscription = cx.on_blur(
235 &focus_handle,
236 window,
237 |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
238 );
239 window.refresh();
240
241 (builder.clone())(
242 Self {
243 builder: Some(builder),
244 items: Default::default(),
245 focus_handle,
246 action_context: None,
247 selected_index: None,
248 delayed: false,
249 clicked: false,
250 key_context: "menu".into(),
251 _on_blur_subscription,
252 keep_open_on_confirm: true,
253 eager: false,
254 documentation_aside: None,
255 fixed_width: None,
256 end_slot_action: None,
257 },
258 window,
259 cx,
260 )
261 })
262 }
263
264 pub fn build_eager(
265 window: &mut Window,
266 cx: &mut App,
267 f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
268 ) -> Entity<Self> {
269 cx.new(|cx| {
270 let focus_handle = cx.focus_handle();
271 let _on_blur_subscription = cx.on_blur(
272 &focus_handle,
273 window,
274 |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
275 );
276 window.refresh();
277 f(
278 Self {
279 builder: None,
280 items: Default::default(),
281 focus_handle,
282 action_context: None,
283 selected_index: None,
284 delayed: false,
285 clicked: false,
286 key_context: "menu".into(),
287 _on_blur_subscription,
288 keep_open_on_confirm: false,
289 eager: true,
290 documentation_aside: None,
291 fixed_width: None,
292 end_slot_action: None,
293 },
294 window,
295 cx,
296 )
297 })
298 }
299
300 /// Rebuilds the menu.
301 ///
302 /// This is used to refresh the menu entries when entries are toggled when the menu is configured with
303 /// `keep_open_on_confirm = true`.
304 ///
305 /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is
306 /// a no-op.
307 pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context<Self>) {
308 let Some(builder) = self.builder.clone() else {
309 return;
310 };
311
312 // The way we rebuild the menu is a bit of a hack.
313 let focus_handle = cx.focus_handle();
314 let new_menu = (builder.clone())(
315 Self {
316 builder: Some(builder),
317 items: Default::default(),
318 focus_handle: focus_handle.clone(),
319 action_context: None,
320 selected_index: None,
321 delayed: false,
322 clicked: false,
323 key_context: "menu".into(),
324 _on_blur_subscription: cx.on_blur(
325 &focus_handle,
326 window,
327 |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
328 ),
329 keep_open_on_confirm: false,
330 eager: false,
331 documentation_aside: None,
332 fixed_width: None,
333 end_slot_action: None,
334 },
335 window,
336 cx,
337 );
338
339 self.items = new_menu.items;
340
341 cx.notify();
342 }
343
344 pub fn context(mut self, focus: FocusHandle) -> Self {
345 self.action_context = Some(focus);
346 self
347 }
348
349 pub fn header(mut self, title: impl Into<SharedString>) -> Self {
350 self.items.push(ContextMenuItem::Header(title.into()));
351 self
352 }
353
354 pub fn header_with_link(
355 mut self,
356 title: impl Into<SharedString>,
357 link_label: impl Into<SharedString>,
358 link_url: impl Into<SharedString>,
359 ) -> Self {
360 self.items.push(ContextMenuItem::HeaderWithLink(
361 title.into(),
362 link_label.into(),
363 link_url.into(),
364 ));
365 self
366 }
367
368 pub fn separator(mut self) -> Self {
369 self.items.push(ContextMenuItem::Separator);
370 self
371 }
372
373 pub fn extend<I: Into<ContextMenuItem>>(mut self, items: impl IntoIterator<Item = I>) -> Self {
374 self.items.extend(items.into_iter().map(Into::into));
375 self
376 }
377
378 pub fn item(mut self, item: impl Into<ContextMenuItem>) -> Self {
379 self.items.push(item.into());
380 self
381 }
382
383 pub fn entry(
384 mut self,
385 label: impl Into<SharedString>,
386 action: Option<Box<dyn Action>>,
387 handler: impl Fn(&mut Window, &mut App) + 'static,
388 ) -> Self {
389 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
390 toggle: None,
391 label: label.into(),
392 handler: Rc::new(move |_, window, cx| handler(window, cx)),
393 icon: None,
394 icon_position: IconPosition::End,
395 icon_size: IconSize::Small,
396 icon_color: None,
397 action,
398 disabled: false,
399 documentation_aside: None,
400 end_slot_icon: None,
401 end_slot_title: None,
402 end_slot_handler: None,
403 show_end_slot_on_hover: false,
404 }));
405 self
406 }
407
408 pub fn entry_with_end_slot(
409 mut self,
410 label: impl Into<SharedString>,
411 action: Option<Box<dyn Action>>,
412 handler: impl Fn(&mut Window, &mut App) + 'static,
413 end_slot_icon: IconName,
414 end_slot_title: SharedString,
415 end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
416 ) -> Self {
417 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
418 toggle: None,
419 label: label.into(),
420 handler: Rc::new(move |_, window, cx| handler(window, cx)),
421 icon: None,
422 icon_position: IconPosition::End,
423 icon_size: IconSize::Small,
424 icon_color: None,
425 action,
426 disabled: false,
427 documentation_aside: None,
428 end_slot_icon: Some(end_slot_icon),
429 end_slot_title: Some(end_slot_title),
430 end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
431 show_end_slot_on_hover: false,
432 }));
433 self
434 }
435
436 pub fn entry_with_end_slot_on_hover(
437 mut self,
438 label: impl Into<SharedString>,
439 action: Option<Box<dyn Action>>,
440 handler: impl Fn(&mut Window, &mut App) + 'static,
441 end_slot_icon: IconName,
442 end_slot_title: SharedString,
443 end_slot_handler: impl Fn(&mut Window, &mut App) + 'static,
444 ) -> Self {
445 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
446 toggle: None,
447 label: label.into(),
448 handler: Rc::new(move |_, window, cx| handler(window, cx)),
449 icon: None,
450 icon_position: IconPosition::End,
451 icon_size: IconSize::Small,
452 icon_color: None,
453 action,
454 disabled: false,
455 documentation_aside: None,
456 end_slot_icon: Some(end_slot_icon),
457 end_slot_title: Some(end_slot_title),
458 end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))),
459 show_end_slot_on_hover: true,
460 }));
461 self
462 }
463
464 pub fn toggleable_entry(
465 mut self,
466 label: impl Into<SharedString>,
467 toggled: bool,
468 position: IconPosition,
469 action: Option<Box<dyn Action>>,
470 handler: impl Fn(&mut Window, &mut App) + 'static,
471 ) -> Self {
472 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
473 toggle: Some((position, toggled)),
474 label: label.into(),
475 handler: Rc::new(move |_, window, cx| handler(window, cx)),
476 icon: None,
477 icon_position: position,
478 icon_size: IconSize::Small,
479 icon_color: None,
480 action,
481 disabled: false,
482 documentation_aside: None,
483 end_slot_icon: None,
484 end_slot_title: None,
485 end_slot_handler: None,
486 show_end_slot_on_hover: false,
487 }));
488 self
489 }
490
491 pub fn custom_row(
492 mut self,
493 entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
494 ) -> Self {
495 self.items.push(ContextMenuItem::CustomEntry {
496 entry_render: Box::new(entry_render),
497 handler: Rc::new(|_, _, _| {}),
498 selectable: false,
499 });
500 self
501 }
502
503 pub fn custom_entry(
504 mut self,
505 entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
506 handler: impl Fn(&mut Window, &mut App) + 'static,
507 ) -> Self {
508 self.items.push(ContextMenuItem::CustomEntry {
509 entry_render: Box::new(entry_render),
510 handler: Rc::new(move |_, window, cx| handler(window, cx)),
511 selectable: true,
512 });
513 self
514 }
515
516 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
517 self.items.push(ContextMenuItem::Label(label.into()));
518 self
519 }
520
521 pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
522 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
523 toggle: None,
524 label: label.into(),
525 action: Some(action.boxed_clone()),
526 handler: Rc::new(move |context, window, cx| {
527 if let Some(context) = &context {
528 window.focus(context);
529 }
530 window.dispatch_action(action.boxed_clone(), cx);
531 }),
532 icon: None,
533 icon_position: IconPosition::End,
534 icon_size: IconSize::Small,
535 icon_color: None,
536 disabled: false,
537 documentation_aside: None,
538 end_slot_icon: None,
539 end_slot_title: None,
540 end_slot_handler: None,
541 show_end_slot_on_hover: false,
542 }));
543 self
544 }
545
546 pub fn disabled_action(
547 mut self,
548 label: impl Into<SharedString>,
549 action: Box<dyn Action>,
550 ) -> Self {
551 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
552 toggle: None,
553 label: label.into(),
554 action: Some(action.boxed_clone()),
555 handler: Rc::new(move |context, window, cx| {
556 if let Some(context) = &context {
557 window.focus(context);
558 }
559 window.dispatch_action(action.boxed_clone(), cx);
560 }),
561 icon: None,
562 icon_size: IconSize::Small,
563 icon_position: IconPosition::End,
564 icon_color: None,
565 disabled: true,
566 documentation_aside: None,
567 end_slot_icon: None,
568 end_slot_title: None,
569 end_slot_handler: None,
570 show_end_slot_on_hover: false,
571 }));
572 self
573 }
574
575 pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
576 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
577 toggle: None,
578 label: label.into(),
579 action: Some(action.boxed_clone()),
580 handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
581 icon: Some(IconName::ArrowUpRight),
582 icon_size: IconSize::XSmall,
583 icon_position: IconPosition::End,
584 icon_color: None,
585 disabled: false,
586 documentation_aside: None,
587 end_slot_icon: None,
588 end_slot_title: None,
589 end_slot_handler: None,
590 show_end_slot_on_hover: false,
591 }));
592 self
593 }
594
595 pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
596 self.keep_open_on_confirm = keep_open;
597 self
598 }
599
600 pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
601 let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
602 return;
603 };
604 let ContextMenuItem::Entry(entry) = entry else {
605 return;
606 };
607 let Some(handler) = entry.end_slot_handler.as_ref() else {
608 return;
609 };
610 handler(None, window, cx);
611 }
612
613 pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
614 self.fixed_width = Some(width);
615 self
616 }
617
618 pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
619 self.end_slot_action = Some(action);
620 self
621 }
622
623 pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
624 self.key_context = context.into();
625 self
626 }
627
628 pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
629 let context = self.action_context.as_ref();
630 if let Some(
631 ContextMenuItem::Entry(ContextMenuEntry {
632 handler,
633 disabled: false,
634 ..
635 })
636 | ContextMenuItem::CustomEntry { handler, .. },
637 ) = self
638 .selected_index
639 .and_then(|ix| self.items.get(ix))
640 .filter(|_| !self.eager)
641 {
642 (handler)(context, window, cx)
643 }
644
645 if self.keep_open_on_confirm {
646 self.rebuild(window, cx);
647 } else {
648 cx.emit(DismissEvent);
649 }
650 }
651
652 pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
653 cx.emit(DismissEvent);
654 cx.emit(DismissEvent);
655 }
656
657 pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
658 let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
659 return;
660 };
661 let ContextMenuItem::Entry(entry) = item else {
662 return;
663 };
664 let Some(handler) = entry.end_slot_handler.as_ref() else {
665 return;
666 };
667 handler(None, window, cx);
668 self.rebuild(window, cx);
669 cx.notify();
670 }
671
672 pub fn clear_selected(&mut self) {
673 self.selected_index = None;
674 }
675
676 pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
677 if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
678 self.select_index(ix, window, cx);
679 }
680 cx.notify();
681 }
682
683 pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
684 for (ix, item) in self.items.iter().enumerate().rev() {
685 if item.is_selectable() {
686 return self.select_index(ix, window, cx);
687 }
688 }
689 None
690 }
691
692 fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
693 if self.select_last(window, cx).is_some() {
694 cx.notify();
695 }
696 }
697
698 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
699 if let Some(ix) = self.selected_index {
700 let next_index = ix + 1;
701 if self.items.len() <= next_index {
702 self.select_first(&SelectFirst, window, cx);
703 } else {
704 for (ix, item) in self.items.iter().enumerate().skip(next_index) {
705 if item.is_selectable() {
706 self.select_index(ix, window, cx);
707 cx.notify();
708 break;
709 }
710 }
711 }
712 } else {
713 self.select_first(&SelectFirst, window, cx);
714 }
715 }
716
717 pub fn select_previous(
718 &mut self,
719 _: &SelectPrevious,
720 window: &mut Window,
721 cx: &mut Context<Self>,
722 ) {
723 if let Some(ix) = self.selected_index {
724 if ix == 0 {
725 self.handle_select_last(&SelectLast, window, cx);
726 } else {
727 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
728 if item.is_selectable() {
729 self.select_index(ix, window, cx);
730 cx.notify();
731 break;
732 }
733 }
734 }
735 } else {
736 self.handle_select_last(&SelectLast, window, cx);
737 }
738 }
739
740 fn select_index(
741 &mut self,
742 ix: usize,
743 window: &mut Window,
744 cx: &mut Context<Self>,
745 ) -> Option<usize> {
746 let context = self.action_context.as_ref();
747 self.documentation_aside = None;
748 let item = self.items.get(ix)?;
749 if item.is_selectable() {
750 self.selected_index = Some(ix);
751 if let ContextMenuItem::Entry(entry) = item {
752 if let Some(callback) = &entry.documentation_aside {
753 self.documentation_aside = Some((ix, callback.clone()));
754 }
755 if self.eager && !entry.disabled {
756 (entry.handler)(context, window, cx)
757 }
758 }
759 }
760 Some(ix)
761 }
762
763 pub fn on_action_dispatch(
764 &mut self,
765 dispatched: &dyn Action,
766 window: &mut Window,
767 cx: &mut Context<Self>,
768 ) {
769 if self.clicked {
770 cx.propagate();
771 return;
772 }
773
774 if let Some(ix) = self.items.iter().position(|item| {
775 if let ContextMenuItem::Entry(ContextMenuEntry {
776 action: Some(action),
777 disabled: false,
778 ..
779 }) = item
780 {
781 action.partial_eq(dispatched)
782 } else {
783 false
784 }
785 }) {
786 self.select_index(ix, window, cx);
787 self.delayed = true;
788 cx.notify();
789 let action = dispatched.boxed_clone();
790 cx.spawn_in(window, async move |this, cx| {
791 cx.background_executor()
792 .timer(Duration::from_millis(50))
793 .await;
794 cx.update(|window, cx| {
795 this.update(cx, |this, cx| {
796 this.cancel(&menu::Cancel, window, cx);
797 window.dispatch_action(action, cx);
798 })
799 })
800 })
801 .detach_and_log_err(cx);
802 } else {
803 cx.propagate()
804 }
805 }
806
807 pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
808 self._on_blur_subscription = new_subscription;
809 self
810 }
811
812 fn render_menu_item(
813 &self,
814 ix: usize,
815 item: &ContextMenuItem,
816 window: &mut Window,
817 cx: &mut Context<Self>,
818 ) -> impl IntoElement + use<> {
819 match item {
820 ContextMenuItem::Separator => ListSeparator.into_any_element(),
821 ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
822 .inset(true)
823 .into_any_element(),
824 ContextMenuItem::HeaderWithLink(header, label, url) => {
825 let url = url.clone();
826 let link_id = ElementId::Name(format!("link-{}", url).into());
827 ListSubHeader::new(header.clone())
828 .inset(true)
829 .end_slot(
830 Button::new(link_id, label.clone())
831 .color(Color::Muted)
832 .label_size(LabelSize::Small)
833 .size(ButtonSize::None)
834 .style(ButtonStyle::Transparent)
835 .on_click(move |_, _, cx| {
836 let url = url.clone();
837 cx.open_url(&url);
838 })
839 .into_any_element(),
840 )
841 .into_any_element()
842 }
843 ContextMenuItem::Label(label) => ListItem::new(ix)
844 .inset(true)
845 .disabled(true)
846 .child(Label::new(label.clone()))
847 .into_any_element(),
848 ContextMenuItem::Entry(entry) => self
849 .render_menu_entry(ix, entry, window, cx)
850 .into_any_element(),
851 ContextMenuItem::CustomEntry {
852 entry_render,
853 handler,
854 selectable,
855 } => {
856 let handler = handler.clone();
857 let menu = cx.entity().downgrade();
858 let selectable = *selectable;
859 ListItem::new(ix)
860 .inset(true)
861 .toggle_state(if selectable {
862 Some(ix) == self.selected_index
863 } else {
864 false
865 })
866 .selectable(selectable)
867 .when(selectable, |item| {
868 item.on_click({
869 let context = self.action_context.clone();
870 let keep_open_on_confirm = self.keep_open_on_confirm;
871 move |_, window, cx| {
872 handler(context.as_ref(), window, cx);
873 menu.update(cx, |menu, cx| {
874 menu.clicked = true;
875
876 if keep_open_on_confirm {
877 menu.rebuild(window, cx);
878 } else {
879 cx.emit(DismissEvent);
880 }
881 })
882 .ok();
883 }
884 })
885 })
886 .child(entry_render(window, cx))
887 .into_any_element()
888 }
889 }
890 }
891
892 fn render_menu_entry(
893 &self,
894 ix: usize,
895 entry: &ContextMenuEntry,
896 window: &mut Window,
897 cx: &mut Context<Self>,
898 ) -> impl IntoElement {
899 let ContextMenuEntry {
900 toggle,
901 label,
902 handler,
903 icon,
904 icon_position,
905 icon_size,
906 icon_color,
907 action,
908 disabled,
909 documentation_aside,
910 end_slot_icon,
911 end_slot_title,
912 end_slot_handler,
913 show_end_slot_on_hover,
914 } = entry;
915 let this = cx.weak_entity();
916
917 let handler = handler.clone();
918 let menu = cx.entity().downgrade();
919
920 let icon_color = if *disabled {
921 Color::Muted
922 } else if toggle.is_some() {
923 icon_color.unwrap_or(Color::Accent)
924 } else {
925 icon_color.unwrap_or(Color::Default)
926 };
927
928 let label_color = if *disabled {
929 Color::Disabled
930 } else {
931 Color::Default
932 };
933
934 let label_element = if let Some(icon_name) = icon {
935 h_flex()
936 .gap_1p5()
937 .when(
938 *icon_position == IconPosition::Start && toggle.is_none(),
939 |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
940 )
941 .child(Label::new(label.clone()).color(label_color).truncate())
942 .when(*icon_position == IconPosition::End, |flex| {
943 flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
944 })
945 .into_any_element()
946 } else {
947 Label::new(label.clone())
948 .color(label_color)
949 .truncate()
950 .into_any_element()
951 };
952
953 div()
954 .id(("context-menu-child", ix))
955 .when_some(documentation_aside.clone(), |this, documentation_aside| {
956 this.occlude()
957 .on_hover(cx.listener(move |menu, hovered, _, cx| {
958 if *hovered {
959 menu.documentation_aside = Some((ix, documentation_aside.clone()));
960 } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
961 menu.documentation_aside = None;
962 }
963 cx.notify();
964 }))
965 })
966 .child(
967 ListItem::new(ix)
968 .group_name("label_container")
969 .inset(true)
970 .disabled(*disabled)
971 .toggle_state(Some(ix) == self.selected_index)
972 .when_some(*toggle, |list_item, (position, toggled)| {
973 let contents = div()
974 .flex_none()
975 .child(
976 Icon::new(icon.unwrap_or(IconName::Check))
977 .color(icon_color)
978 .size(*icon_size),
979 )
980 .when(!toggled, |contents| contents.invisible());
981
982 match position {
983 IconPosition::Start => list_item.start_slot(contents),
984 IconPosition::End => list_item.end_slot(contents),
985 }
986 })
987 .child(
988 h_flex()
989 .w_full()
990 .justify_between()
991 .child(label_element)
992 .debug_selector(|| format!("MENU_ITEM-{}", label))
993 .children(action.as_ref().and_then(|action| {
994 self.action_context
995 .as_ref()
996 .map(|focus| {
997 KeyBinding::for_action_in(&**action, focus, window, cx)
998 })
999 .unwrap_or_else(|| {
1000 KeyBinding::for_action(&**action, window, cx)
1001 })
1002 .map(|binding| {
1003 div().ml_4().child(binding.disabled(*disabled)).when(
1004 *disabled && documentation_aside.is_some(),
1005 |parent| parent.invisible(),
1006 )
1007 })
1008 }))
1009 .when(*disabled && documentation_aside.is_some(), |parent| {
1010 parent.child(
1011 Icon::new(IconName::Info)
1012 .size(IconSize::XSmall)
1013 .color(Color::Muted),
1014 )
1015 }),
1016 )
1017 .when_some(
1018 end_slot_icon
1019 .as_ref()
1020 .zip(self.end_slot_action.as_ref())
1021 .zip(end_slot_title.as_ref())
1022 .zip(end_slot_handler.as_ref()),
1023 |el, (((icon, action), title), handler)| {
1024 el.end_slot({
1025 let icon_button = IconButton::new("end-slot-icon", *icon)
1026 .shape(IconButtonShape::Square)
1027 .tooltip({
1028 let action_context = self.action_context.clone();
1029 let title = title.clone();
1030 let action = action.boxed_clone();
1031 move |window, cx| {
1032 action_context
1033 .as_ref()
1034 .map(|focus| {
1035 Tooltip::for_action_in(
1036 title.clone(),
1037 &*action,
1038 focus,
1039 window,
1040 cx,
1041 )
1042 })
1043 .unwrap_or_else(|| {
1044 Tooltip::for_action(
1045 title.clone(),
1046 &*action,
1047 window,
1048 cx,
1049 )
1050 })
1051 }
1052 })
1053 .on_click({
1054 let handler = handler.clone();
1055 move |_, window, cx| {
1056 handler(None, window, cx);
1057 this.update(cx, |this, cx| {
1058 this.rebuild(window, cx);
1059 cx.notify();
1060 })
1061 .ok();
1062 }
1063 });
1064
1065 if *show_end_slot_on_hover {
1066 div()
1067 .visible_on_hover("label_container")
1068 .child(icon_button)
1069 .into_any_element()
1070 } else {
1071 icon_button.into_any_element()
1072 }
1073 })
1074 },
1075 )
1076 .on_click({
1077 let context = self.action_context.clone();
1078 let keep_open_on_confirm = self.keep_open_on_confirm;
1079 move |_, window, cx| {
1080 handler(context.as_ref(), window, cx);
1081 menu.update(cx, |menu, cx| {
1082 menu.clicked = true;
1083 if keep_open_on_confirm {
1084 menu.rebuild(window, cx);
1085 } else {
1086 cx.emit(DismissEvent);
1087 }
1088 })
1089 .ok();
1090 }
1091 }),
1092 )
1093 .into_any_element()
1094 }
1095}
1096
1097impl ContextMenuItem {
1098 fn is_selectable(&self) -> bool {
1099 match self {
1100 ContextMenuItem::Header(_)
1101 | ContextMenuItem::HeaderWithLink(_, _, _)
1102 | ContextMenuItem::Separator
1103 | ContextMenuItem::Label { .. } => false,
1104 ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
1105 ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
1106 }
1107 }
1108}
1109
1110impl Render for ContextMenu {
1111 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1112 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1113 let window_size = window.viewport_size();
1114 let rem_size = window.rem_size();
1115 let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
1116
1117 let aside = self.documentation_aside.clone();
1118 let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
1119 WithRemSize::new(ui_font_size)
1120 .occlude()
1121 .elevation_2(cx)
1122 .p_2()
1123 .overflow_hidden()
1124 .when(is_wide_window, |this| this.max_w_96())
1125 .when(!is_wide_window, |this| this.max_w_48())
1126 .child((aside.render)(cx))
1127 };
1128
1129 h_flex()
1130 .when(is_wide_window, |this| this.flex_row())
1131 .when(!is_wide_window, |this| this.flex_col())
1132 .w_full()
1133 .items_start()
1134 .gap_1()
1135 .child(div().children(aside.clone().and_then(|(_, aside)| {
1136 (aside.side == DocumentationSide::Left).then(|| render_aside(aside, cx))
1137 })))
1138 .child(
1139 WithRemSize::new(ui_font_size)
1140 .occlude()
1141 .elevation_2(cx)
1142 .flex()
1143 .flex_row()
1144 .child(
1145 v_flex()
1146 .id("context-menu")
1147 .max_h(vh(0.75, window))
1148 .when_some(self.fixed_width, |this, width| {
1149 this.w(width).overflow_x_hidden()
1150 })
1151 .when(self.fixed_width.is_none(), |this| {
1152 this.min_w(px(200.)).flex_1()
1153 })
1154 .overflow_y_scroll()
1155 .track_focus(&self.focus_handle(cx))
1156 .on_mouse_down_out(cx.listener(|this, _, window, cx| {
1157 this.cancel(&menu::Cancel, window, cx)
1158 }))
1159 .key_context(self.key_context.as_ref())
1160 .on_action(cx.listener(ContextMenu::select_first))
1161 .on_action(cx.listener(ContextMenu::handle_select_last))
1162 .on_action(cx.listener(ContextMenu::select_next))
1163 .on_action(cx.listener(ContextMenu::select_previous))
1164 .on_action(cx.listener(ContextMenu::confirm))
1165 .on_action(cx.listener(ContextMenu::cancel))
1166 .when_some(self.end_slot_action.as_ref(), |el, action| {
1167 el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
1168 })
1169 .when(!self.delayed, |mut el| {
1170 for item in self.items.iter() {
1171 if let ContextMenuItem::Entry(ContextMenuEntry {
1172 action: Some(action),
1173 disabled: false,
1174 ..
1175 }) = item
1176 {
1177 el = el.on_boxed_action(
1178 &**action,
1179 cx.listener(ContextMenu::on_action_dispatch),
1180 );
1181 }
1182 }
1183 el
1184 })
1185 .child(
1186 List::new().children(
1187 self.items.iter().enumerate().map(|(ix, item)| {
1188 self.render_menu_item(ix, item, window, cx)
1189 }),
1190 ),
1191 ),
1192 ),
1193 )
1194 .child(div().children(aside.and_then(|(_, aside)| {
1195 (aside.side == DocumentationSide::Right).then(|| render_aside(aside, cx))
1196 })))
1197 }
1198}