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