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