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 action_disabled_when(
507 mut self,
508 disabled: bool,
509 label: impl Into<SharedString>,
510 action: Box<dyn Action>,
511 ) -> Self {
512 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
513 toggle: None,
514 label: label.into(),
515 action: Some(action.boxed_clone()),
516 handler: Rc::new(move |context, window, cx| {
517 if let Some(context) = &context {
518 window.focus(context);
519 }
520 window.dispatch_action(action.boxed_clone(), cx);
521 }),
522 icon: None,
523 icon_size: IconSize::Small,
524 icon_position: IconPosition::End,
525 icon_color: None,
526 disabled,
527 documentation_aside: None,
528 end_slot_icon: None,
529 end_slot_title: None,
530 end_slot_handler: None,
531 show_end_slot_on_hover: false,
532 }));
533 self
534 }
535
536 pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
537 self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
538 toggle: None,
539 label: label.into(),
540 action: Some(action.boxed_clone()),
541 handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
542 icon: Some(IconName::ArrowUpRight),
543 icon_size: IconSize::XSmall,
544 icon_position: IconPosition::End,
545 icon_color: None,
546 disabled: false,
547 documentation_aside: None,
548 end_slot_icon: None,
549 end_slot_title: None,
550 end_slot_handler: None,
551 show_end_slot_on_hover: false,
552 }));
553 self
554 }
555
556 pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self {
557 self.keep_open_on_confirm = keep_open;
558 self
559 }
560
561 pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context<Self>) {
562 let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
563 return;
564 };
565 let ContextMenuItem::Entry(entry) = entry else {
566 return;
567 };
568 let Some(handler) = entry.end_slot_handler.as_ref() else {
569 return;
570 };
571 handler(None, window, cx);
572 }
573
574 pub fn fixed_width(mut self, width: DefiniteLength) -> Self {
575 self.fixed_width = Some(width);
576 self
577 }
578
579 pub fn end_slot_action(mut self, action: Box<dyn Action>) -> Self {
580 self.end_slot_action = Some(action);
581 self
582 }
583
584 pub fn key_context(mut self, context: impl Into<SharedString>) -> Self {
585 self.key_context = context.into();
586 self
587 }
588
589 pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
590 let context = self.action_context.as_ref();
591 if let Some(
592 ContextMenuItem::Entry(ContextMenuEntry {
593 handler,
594 disabled: false,
595 ..
596 })
597 | ContextMenuItem::CustomEntry { handler, .. },
598 ) = self.selected_index.and_then(|ix| self.items.get(ix))
599 {
600 (handler)(context, window, cx)
601 }
602
603 if self.keep_open_on_confirm {
604 self.rebuild(window, cx);
605 } else {
606 cx.emit(DismissEvent);
607 }
608 }
609
610 pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
611 cx.emit(DismissEvent);
612 cx.emit(DismissEvent);
613 }
614
615 pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
616 let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else {
617 return;
618 };
619 let ContextMenuItem::Entry(entry) = item else {
620 return;
621 };
622 let Some(handler) = entry.end_slot_handler.as_ref() else {
623 return;
624 };
625 handler(None, window, cx);
626 self.rebuild(window, cx);
627 cx.notify();
628 }
629
630 pub fn clear_selected(&mut self) {
631 self.selected_index = None;
632 }
633
634 pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
635 if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
636 self.select_index(ix, window, cx);
637 }
638 cx.notify();
639 }
640
641 pub fn select_last(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<usize> {
642 for (ix, item) in self.items.iter().enumerate().rev() {
643 if item.is_selectable() {
644 return self.select_index(ix, window, cx);
645 }
646 }
647 None
648 }
649
650 fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
651 if self.select_last(window, cx).is_some() {
652 cx.notify();
653 }
654 }
655
656 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
657 if let Some(ix) = self.selected_index {
658 let next_index = ix + 1;
659 if self.items.len() <= next_index {
660 self.select_first(&SelectFirst, window, cx);
661 } else {
662 for (ix, item) in self.items.iter().enumerate().skip(next_index) {
663 if item.is_selectable() {
664 self.select_index(ix, window, cx);
665 cx.notify();
666 break;
667 }
668 }
669 }
670 } else {
671 self.select_first(&SelectFirst, window, cx);
672 }
673 }
674
675 pub fn select_previous(
676 &mut self,
677 _: &SelectPrevious,
678 window: &mut Window,
679 cx: &mut Context<Self>,
680 ) {
681 if let Some(ix) = self.selected_index {
682 if ix == 0 {
683 self.handle_select_last(&SelectLast, window, cx);
684 } else {
685 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
686 if item.is_selectable() {
687 self.select_index(ix, window, cx);
688 cx.notify();
689 break;
690 }
691 }
692 }
693 } else {
694 self.handle_select_last(&SelectLast, window, cx);
695 }
696 }
697
698 fn select_index(
699 &mut self,
700 ix: usize,
701 _window: &mut Window,
702 _cx: &mut Context<Self>,
703 ) -> Option<usize> {
704 self.documentation_aside = None;
705 let item = self.items.get(ix)?;
706 if item.is_selectable() {
707 self.selected_index = Some(ix);
708 if let ContextMenuItem::Entry(entry) = item {
709 if let Some(callback) = &entry.documentation_aside {
710 self.documentation_aside = Some((ix, callback.clone()));
711 }
712 }
713 }
714 Some(ix)
715 }
716
717 pub fn on_action_dispatch(
718 &mut self,
719 dispatched: &dyn Action,
720 window: &mut Window,
721 cx: &mut Context<Self>,
722 ) {
723 if self.clicked {
724 cx.propagate();
725 return;
726 }
727
728 if let Some(ix) = self.items.iter().position(|item| {
729 if let ContextMenuItem::Entry(ContextMenuEntry {
730 action: Some(action),
731 disabled: false,
732 ..
733 }) = item
734 {
735 action.partial_eq(dispatched)
736 } else {
737 false
738 }
739 }) {
740 self.select_index(ix, window, cx);
741 self.delayed = true;
742 cx.notify();
743 let action = dispatched.boxed_clone();
744 cx.spawn_in(window, async move |this, cx| {
745 cx.background_executor()
746 .timer(Duration::from_millis(50))
747 .await;
748 cx.update(|window, cx| {
749 this.update(cx, |this, cx| {
750 this.cancel(&menu::Cancel, window, cx);
751 window.dispatch_action(action, cx);
752 })
753 })
754 })
755 .detach_and_log_err(cx);
756 } else {
757 cx.propagate()
758 }
759 }
760
761 pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self {
762 self._on_blur_subscription = new_subscription;
763 self
764 }
765
766 fn render_menu_item(
767 &self,
768 ix: usize,
769 item: &ContextMenuItem,
770 window: &mut Window,
771 cx: &mut Context<Self>,
772 ) -> impl IntoElement + use<> {
773 match item {
774 ContextMenuItem::Separator => ListSeparator.into_any_element(),
775 ContextMenuItem::Header(header) => ListSubHeader::new(header.clone())
776 .inset(true)
777 .into_any_element(),
778 ContextMenuItem::HeaderWithLink(header, label, url) => {
779 let url = url.clone();
780 let link_id = ElementId::Name(format!("link-{}", url).into());
781 ListSubHeader::new(header.clone())
782 .inset(true)
783 .end_slot(
784 Button::new(link_id, label.clone())
785 .color(Color::Muted)
786 .label_size(LabelSize::Small)
787 .size(ButtonSize::None)
788 .style(ButtonStyle::Transparent)
789 .on_click(move |_, _, cx| {
790 let url = url.clone();
791 cx.open_url(&url);
792 })
793 .into_any_element(),
794 )
795 .into_any_element()
796 }
797 ContextMenuItem::Label(label) => ListItem::new(ix)
798 .inset(true)
799 .disabled(true)
800 .child(Label::new(label.clone()))
801 .into_any_element(),
802 ContextMenuItem::Entry(entry) => self
803 .render_menu_entry(ix, entry, window, cx)
804 .into_any_element(),
805 ContextMenuItem::CustomEntry {
806 entry_render,
807 handler,
808 selectable,
809 } => {
810 let handler = handler.clone();
811 let menu = cx.entity().downgrade();
812 let selectable = *selectable;
813 ListItem::new(ix)
814 .inset(true)
815 .toggle_state(if selectable {
816 Some(ix) == self.selected_index
817 } else {
818 false
819 })
820 .selectable(selectable)
821 .when(selectable, |item| {
822 item.on_click({
823 let context = self.action_context.clone();
824 let keep_open_on_confirm = self.keep_open_on_confirm;
825 move |_, window, cx| {
826 handler(context.as_ref(), window, cx);
827 menu.update(cx, |menu, cx| {
828 menu.clicked = true;
829
830 if keep_open_on_confirm {
831 menu.rebuild(window, cx);
832 } else {
833 cx.emit(DismissEvent);
834 }
835 })
836 .ok();
837 }
838 })
839 })
840 .child(entry_render(window, cx))
841 .into_any_element()
842 }
843 }
844 }
845
846 fn render_menu_entry(
847 &self,
848 ix: usize,
849 entry: &ContextMenuEntry,
850 window: &mut Window,
851 cx: &mut Context<Self>,
852 ) -> impl IntoElement {
853 let ContextMenuEntry {
854 toggle,
855 label,
856 handler,
857 icon,
858 icon_position,
859 icon_size,
860 icon_color,
861 action,
862 disabled,
863 documentation_aside,
864 end_slot_icon,
865 end_slot_title,
866 end_slot_handler,
867 show_end_slot_on_hover,
868 } = entry;
869 let this = cx.weak_entity();
870
871 let handler = handler.clone();
872 let menu = cx.entity().downgrade();
873
874 let icon_color = if *disabled {
875 Color::Muted
876 } else if toggle.is_some() {
877 icon_color.unwrap_or(Color::Accent)
878 } else {
879 icon_color.unwrap_or(Color::Default)
880 };
881
882 let label_color = if *disabled {
883 Color::Disabled
884 } else {
885 Color::Default
886 };
887
888 let label_element = if let Some(icon_name) = icon {
889 h_flex()
890 .gap_1p5()
891 .when(
892 *icon_position == IconPosition::Start && toggle.is_none(),
893 |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)),
894 )
895 .child(Label::new(label.clone()).color(label_color).truncate())
896 .when(*icon_position == IconPosition::End, |flex| {
897 flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color))
898 })
899 .into_any_element()
900 } else {
901 Label::new(label.clone())
902 .color(label_color)
903 .truncate()
904 .into_any_element()
905 };
906
907 div()
908 .id(("context-menu-child", ix))
909 .when_some(documentation_aside.clone(), |this, documentation_aside| {
910 this.occlude()
911 .on_hover(cx.listener(move |menu, hovered, _, cx| {
912 if *hovered {
913 menu.documentation_aside = Some((ix, documentation_aside.clone()));
914 } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
915 menu.documentation_aside = None;
916 }
917 cx.notify();
918 }))
919 })
920 .child(
921 ListItem::new(ix)
922 .group_name("label_container")
923 .inset(true)
924 .disabled(*disabled)
925 .toggle_state(Some(ix) == self.selected_index)
926 .when_some(*toggle, |list_item, (position, toggled)| {
927 let contents = div()
928 .flex_none()
929 .child(
930 Icon::new(icon.unwrap_or(IconName::Check))
931 .color(icon_color)
932 .size(*icon_size),
933 )
934 .when(!toggled, |contents| contents.invisible());
935
936 match position {
937 IconPosition::Start => list_item.start_slot(contents),
938 IconPosition::End => list_item.end_slot(contents),
939 }
940 })
941 .child(
942 h_flex()
943 .w_full()
944 .justify_between()
945 .child(label_element)
946 .debug_selector(|| format!("MENU_ITEM-{}", label))
947 .children(action.as_ref().and_then(|action| {
948 self.action_context
949 .as_ref()
950 .map(|focus| {
951 KeyBinding::for_action_in(&**action, focus, window, cx)
952 })
953 .unwrap_or_else(|| {
954 KeyBinding::for_action(&**action, window, cx)
955 })
956 .map(|binding| {
957 div().ml_4().child(binding.disabled(*disabled)).when(
958 *disabled && documentation_aside.is_some(),
959 |parent| parent.invisible(),
960 )
961 })
962 }))
963 .when(*disabled && documentation_aside.is_some(), |parent| {
964 parent.child(
965 Icon::new(IconName::Info)
966 .size(IconSize::XSmall)
967 .color(Color::Muted),
968 )
969 }),
970 )
971 .when_some(
972 end_slot_icon
973 .as_ref()
974 .zip(self.end_slot_action.as_ref())
975 .zip(end_slot_title.as_ref())
976 .zip(end_slot_handler.as_ref()),
977 |el, (((icon, action), title), handler)| {
978 el.end_slot({
979 let icon_button = IconButton::new("end-slot-icon", *icon)
980 .shape(IconButtonShape::Square)
981 .tooltip({
982 let action_context = self.action_context.clone();
983 let title = title.clone();
984 let action = action.boxed_clone();
985 move |window, cx| {
986 action_context
987 .as_ref()
988 .map(|focus| {
989 Tooltip::for_action_in(
990 title.clone(),
991 &*action,
992 focus,
993 window,
994 cx,
995 )
996 })
997 .unwrap_or_else(|| {
998 Tooltip::for_action(
999 title.clone(),
1000 &*action,
1001 window,
1002 cx,
1003 )
1004 })
1005 }
1006 })
1007 .on_click({
1008 let handler = handler.clone();
1009 move |_, window, cx| {
1010 handler(None, window, cx);
1011 this.update(cx, |this, cx| {
1012 this.rebuild(window, cx);
1013 cx.notify();
1014 })
1015 .ok();
1016 }
1017 });
1018
1019 if *show_end_slot_on_hover {
1020 div()
1021 .visible_on_hover("label_container")
1022 .child(icon_button)
1023 .into_any_element()
1024 } else {
1025 icon_button.into_any_element()
1026 }
1027 })
1028 },
1029 )
1030 .on_click({
1031 let context = self.action_context.clone();
1032 let keep_open_on_confirm = self.keep_open_on_confirm;
1033 move |_, window, cx| {
1034 handler(context.as_ref(), window, cx);
1035 menu.update(cx, |menu, cx| {
1036 menu.clicked = true;
1037 if keep_open_on_confirm {
1038 menu.rebuild(window, cx);
1039 } else {
1040 cx.emit(DismissEvent);
1041 }
1042 })
1043 .ok();
1044 }
1045 }),
1046 )
1047 .into_any_element()
1048 }
1049}
1050
1051impl ContextMenuItem {
1052 fn is_selectable(&self) -> bool {
1053 match self {
1054 ContextMenuItem::Header(_)
1055 | ContextMenuItem::HeaderWithLink(_, _, _)
1056 | ContextMenuItem::Separator
1057 | ContextMenuItem::Label { .. } => false,
1058 ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
1059 ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
1060 }
1061 }
1062}
1063
1064impl Render for ContextMenu {
1065 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1066 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1067 let window_size = window.viewport_size();
1068 let rem_size = window.rem_size();
1069 let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
1070
1071 let aside = self.documentation_aside.clone();
1072 let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
1073 WithRemSize::new(ui_font_size)
1074 .occlude()
1075 .elevation_2(cx)
1076 .p_2()
1077 .overflow_hidden()
1078 .when(is_wide_window, |this| this.max_w_96())
1079 .when(!is_wide_window, |this| this.max_w_48())
1080 .child((aside.render)(cx))
1081 };
1082
1083 h_flex()
1084 .when(is_wide_window, |this| this.flex_row())
1085 .when(!is_wide_window, |this| this.flex_col())
1086 .w_full()
1087 .items_start()
1088 .gap_1()
1089 .child(div().children(aside.clone().and_then(|(_, aside)| {
1090 (aside.side == DocumentationSide::Left).then(|| render_aside(aside, cx))
1091 })))
1092 .child(
1093 WithRemSize::new(ui_font_size)
1094 .occlude()
1095 .elevation_2(cx)
1096 .flex()
1097 .flex_row()
1098 .child(
1099 v_flex()
1100 .id("context-menu")
1101 .max_h(vh(0.75, window))
1102 .when_some(self.fixed_width, |this, width| {
1103 this.w(width).overflow_x_hidden()
1104 })
1105 .when(self.fixed_width.is_none(), |this| {
1106 this.min_w(px(200.)).flex_1()
1107 })
1108 .overflow_y_scroll()
1109 .track_focus(&self.focus_handle(cx))
1110 .on_mouse_down_out(cx.listener(|this, _, window, cx| {
1111 this.cancel(&menu::Cancel, window, cx)
1112 }))
1113 .key_context(self.key_context.as_ref())
1114 .on_action(cx.listener(ContextMenu::select_first))
1115 .on_action(cx.listener(ContextMenu::handle_select_last))
1116 .on_action(cx.listener(ContextMenu::select_next))
1117 .on_action(cx.listener(ContextMenu::select_previous))
1118 .on_action(cx.listener(ContextMenu::confirm))
1119 .on_action(cx.listener(ContextMenu::cancel))
1120 .when_some(self.end_slot_action.as_ref(), |el, action| {
1121 el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot))
1122 })
1123 .when(!self.delayed, |mut el| {
1124 for item in self.items.iter() {
1125 if let ContextMenuItem::Entry(ContextMenuEntry {
1126 action: Some(action),
1127 disabled: false,
1128 ..
1129 }) = item
1130 {
1131 el = el.on_boxed_action(
1132 &**action,
1133 cx.listener(ContextMenu::on_action_dispatch),
1134 );
1135 }
1136 }
1137 el
1138 })
1139 .child(
1140 List::new().children(
1141 self.items.iter().enumerate().map(|(ix, item)| {
1142 self.render_menu_item(ix, item, window, cx)
1143 }),
1144 ),
1145 ),
1146 ),
1147 )
1148 .child(div().children(aside.and_then(|(_, aside)| {
1149 (aside.side == DocumentationSide::Right).then(|| render_aside(aside, cx))
1150 })))
1151 }
1152}