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