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