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