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