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