context_menu.rs

  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}