use crate::{ Icon, IconButtonShape, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader, h_flex, prelude::*, utils::WithRemSize, v_flex, }; use gpui::{ Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, Render, Subscription, px, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; use settings::Settings; use std::{rc::Rc, time::Duration}; use theme::ThemeSettings; use super::Tooltip; pub enum ContextMenuItem { Separator, Header(SharedString), /// title, link_label, link_url HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header Label(SharedString), Entry(ContextMenuEntry), CustomEntry { entry_render: Box AnyElement>, handler: Rc, &mut Window, &mut App)>, selectable: bool, }, } impl ContextMenuItem { pub fn custom_entry( entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, handler: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { Self::CustomEntry { entry_render: Box::new(entry_render), handler: Rc::new(move |_, window, cx| handler(window, cx)), selectable: true, } } } pub struct ContextMenuEntry { toggle: Option<(IconPosition, bool)>, label: SharedString, icon: Option, icon_position: IconPosition, icon_size: IconSize, icon_color: Option, handler: Rc, &mut Window, &mut App)>, action: Option>, disabled: bool, documentation_aside: Option, end_slot_icon: Option, end_slot_title: Option, end_slot_handler: Option, &mut Window, &mut App)>>, show_end_slot_on_hover: bool, } impl ContextMenuEntry { pub fn new(label: impl Into) -> Self { ContextMenuEntry { toggle: None, label: label.into(), icon: None, icon_position: IconPosition::Start, icon_size: IconSize::Small, icon_color: None, handler: Rc::new(|_, _, _| {}), action: None, disabled: false, documentation_aside: None, end_slot_icon: None, end_slot_title: None, end_slot_handler: None, show_end_slot_on_hover: false, } } pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self { self.toggle = Some((toggle_position, toggled)); self } pub fn icon(mut self, icon: IconName) -> Self { self.icon = Some(icon); self } pub fn icon_position(mut self, position: IconPosition) -> Self { self.icon_position = position; self } pub fn icon_size(mut self, icon_size: IconSize) -> Self { self.icon_size = icon_size; self } pub fn icon_color(mut self, icon_color: Color) -> Self { self.icon_color = Some(icon_color); self } pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self { self.toggle = Some((toggle_position, toggled)); self } pub fn action(mut self, action: Box) -> Self { self.action = Some(action); self } pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.handler = Rc::new(move |_, window, cx| handler(window, cx)); self } pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } pub fn documentation_aside( mut self, side: DocumentationSide, render: impl Fn(&mut App) -> AnyElement + 'static, ) -> Self { self.documentation_aside = Some(DocumentationAside { side, render: Rc::new(render), }); self } } impl From for ContextMenuItem { fn from(entry: ContextMenuEntry) -> Self { ContextMenuItem::Entry(entry) } } pub struct ContextMenu { builder: Option) -> Self>>, items: Vec, focus_handle: FocusHandle, action_context: Option, selected_index: Option, delayed: bool, clicked: bool, end_slot_action: Option>, key_context: SharedString, _on_blur_subscription: Subscription, keep_open_on_confirm: bool, eager: bool, documentation_aside: Option<(usize, DocumentationAside)>, fixed_width: Option, } #[derive(Copy, Clone, PartialEq, Eq)] pub enum DocumentationSide { Left, Right, } #[derive(Clone)] pub struct DocumentationAside { side: DocumentationSide, render: Rc AnyElement>, } impl Focusable for ContextMenu { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl EventEmitter for ContextMenu {} impl FluentBuilder for ContextMenu {} impl ContextMenu { pub fn build( window: &mut Window, cx: &mut App, f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, ) -> Entity { cx.new(|cx| { let focus_handle = cx.focus_handle(); let _on_blur_subscription = cx.on_blur( &focus_handle, window, |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), ); window.refresh(); f( Self { builder: None, items: Default::default(), focus_handle, action_context: None, selected_index: None, delayed: false, clicked: false, key_context: "menu".into(), _on_blur_subscription, keep_open_on_confirm: false, eager: false, documentation_aside: None, fixed_width: None, end_slot_action: None, }, window, cx, ) }) } /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation. /// /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto /// it to call it again. pub fn build_persistent( window: &mut Window, cx: &mut App, builder: impl Fn(Self, &mut Window, &mut Context) -> Self + 'static, ) -> Entity { cx.new(|cx| { let builder = Rc::new(builder); let focus_handle = cx.focus_handle(); let _on_blur_subscription = cx.on_blur( &focus_handle, window, |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), ); window.refresh(); (builder.clone())( Self { builder: Some(builder), items: Default::default(), focus_handle, action_context: None, selected_index: None, delayed: false, clicked: false, key_context: "menu".into(), _on_blur_subscription, keep_open_on_confirm: true, eager: false, documentation_aside: None, fixed_width: None, end_slot_action: None, }, window, cx, ) }) } pub fn build_eager( window: &mut Window, cx: &mut App, f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, ) -> Entity { cx.new(|cx| { let focus_handle = cx.focus_handle(); let _on_blur_subscription = cx.on_blur( &focus_handle, window, |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), ); window.refresh(); f( Self { builder: None, items: Default::default(), focus_handle, action_context: None, selected_index: None, delayed: false, clicked: false, key_context: "menu".into(), _on_blur_subscription, keep_open_on_confirm: false, eager: true, documentation_aside: None, fixed_width: None, end_slot_action: None, }, window, cx, ) }) } /// Rebuilds the menu. /// /// This is used to refresh the menu entries when entries are toggled when the menu is configured with /// `keep_open_on_confirm = true`. /// /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is /// a no-op. pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context) { let Some(builder) = self.builder.clone() else { return; }; // The way we rebuild the menu is a bit of a hack. let focus_handle = cx.focus_handle(); let new_menu = (builder.clone())( Self { builder: Some(builder), items: Default::default(), focus_handle: focus_handle.clone(), action_context: None, selected_index: None, delayed: false, clicked: false, key_context: "menu".into(), _on_blur_subscription: cx.on_blur( &focus_handle, window, |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), ), keep_open_on_confirm: false, eager: false, documentation_aside: None, fixed_width: None, end_slot_action: None, }, window, cx, ); self.items = new_menu.items; cx.notify(); } pub fn context(mut self, focus: FocusHandle) -> Self { self.action_context = Some(focus); self } pub fn header(mut self, title: impl Into) -> Self { self.items.push(ContextMenuItem::Header(title.into())); self } pub fn header_with_link( mut self, title: impl Into, link_label: impl Into, link_url: impl Into, ) -> Self { self.items.push(ContextMenuItem::HeaderWithLink( title.into(), link_label.into(), link_url.into(), )); self } pub fn separator(mut self) -> Self { self.items.push(ContextMenuItem::Separator); self } pub fn extend>(mut self, items: impl IntoIterator) -> Self { self.items.extend(items.into_iter().map(Into::into)); self } pub fn item(mut self, item: impl Into) -> Self { self.items.push(item.into()); self } pub fn entry( mut self, label: impl Into, action: Option>, handler: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), handler: Rc::new(move |_, window, cx| handler(window, cx)), icon: None, icon_position: IconPosition::End, icon_size: IconSize::Small, icon_color: None, action, disabled: false, documentation_aside: None, end_slot_icon: None, end_slot_title: None, end_slot_handler: None, show_end_slot_on_hover: false, })); self } pub fn entry_with_end_slot( mut self, label: impl Into, action: Option>, handler: impl Fn(&mut Window, &mut App) + 'static, end_slot_icon: IconName, end_slot_title: SharedString, end_slot_handler: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), handler: Rc::new(move |_, window, cx| handler(window, cx)), icon: None, icon_position: IconPosition::End, icon_size: IconSize::Small, icon_color: None, action, disabled: false, documentation_aside: None, end_slot_icon: Some(end_slot_icon), end_slot_title: Some(end_slot_title), end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))), show_end_slot_on_hover: false, })); self } pub fn entry_with_end_slot_on_hover( mut self, label: impl Into, action: Option>, handler: impl Fn(&mut Window, &mut App) + 'static, end_slot_icon: IconName, end_slot_title: SharedString, end_slot_handler: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), handler: Rc::new(move |_, window, cx| handler(window, cx)), icon: None, icon_position: IconPosition::End, icon_size: IconSize::Small, icon_color: None, action, disabled: false, documentation_aside: None, end_slot_icon: Some(end_slot_icon), end_slot_title: Some(end_slot_title), end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))), show_end_slot_on_hover: true, })); self } pub fn toggleable_entry( mut self, label: impl Into, toggled: bool, position: IconPosition, action: Option>, handler: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: Some((position, toggled)), label: label.into(), handler: Rc::new(move |_, window, cx| handler(window, cx)), icon: None, icon_position: position, icon_size: IconSize::Small, icon_color: None, action, disabled: false, documentation_aside: None, end_slot_icon: None, end_slot_title: None, end_slot_handler: None, show_end_slot_on_hover: false, })); self } pub fn custom_row( mut self, entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, ) -> Self { self.items.push(ContextMenuItem::CustomEntry { entry_render: Box::new(entry_render), handler: Rc::new(|_, _, _| {}), selectable: false, }); self } pub fn custom_entry( mut self, entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, handler: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { self.items.push(ContextMenuItem::CustomEntry { entry_render: Box::new(entry_render), handler: Rc::new(move |_, window, cx| handler(window, cx)), selectable: true, }); self } pub fn label(mut self, label: impl Into) -> Self { self.items.push(ContextMenuItem::Label(label.into())); self } pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |context, window, cx| { if let Some(context) = &context { window.focus(context); } window.dispatch_action(action.boxed_clone(), cx); }), icon: None, icon_position: IconPosition::End, icon_size: IconSize::Small, icon_color: None, disabled: false, documentation_aside: None, end_slot_icon: None, end_slot_title: None, end_slot_handler: None, show_end_slot_on_hover: false, })); self } pub fn disabled_action( mut self, label: impl Into, action: Box, ) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |context, window, cx| { if let Some(context) = &context { window.focus(context); } window.dispatch_action(action.boxed_clone(), cx); }), icon: None, icon_size: IconSize::Small, icon_position: IconPosition::End, icon_color: None, disabled: true, documentation_aside: None, end_slot_icon: None, end_slot_title: None, end_slot_handler: None, show_end_slot_on_hover: false, })); self } pub fn link(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), icon_size: IconSize::XSmall, icon_position: IconPosition::End, icon_color: None, disabled: false, documentation_aside: None, end_slot_icon: None, end_slot_title: None, end_slot_handler: None, show_end_slot_on_hover: false, })); self } pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self { self.keep_open_on_confirm = keep_open; self } pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context) { let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else { return; }; let ContextMenuItem::Entry(entry) = entry else { return; }; let Some(handler) = entry.end_slot_handler.as_ref() else { return; }; handler(None, window, cx); } pub fn fixed_width(mut self, width: DefiniteLength) -> Self { self.fixed_width = Some(width); self } pub fn end_slot_action(mut self, action: Box) -> Self { self.end_slot_action = Some(action); self } pub fn key_context(mut self, context: impl Into) -> Self { self.key_context = context.into(); self } pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { let context = self.action_context.as_ref(); if let Some( ContextMenuItem::Entry(ContextMenuEntry { handler, disabled: false, .. }) | ContextMenuItem::CustomEntry { handler, .. }, ) = self .selected_index .and_then(|ix| self.items.get(ix)) .filter(|_| !self.eager) { (handler)(context, window, cx) } if self.keep_open_on_confirm { self.rebuild(window, cx); } else { cx.emit(DismissEvent); } } pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { cx.emit(DismissEvent); cx.emit(DismissEvent); } pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context) { let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else { return; }; let ContextMenuItem::Entry(entry) = item else { return; }; let Some(handler) = entry.end_slot_handler.as_ref() else { return; }; handler(None, window, cx); self.rebuild(window, cx); cx.notify(); } pub fn clear_selected(&mut self) { self.selected_index = None; } pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) { self.select_index(ix, window, cx); } cx.notify(); } pub fn select_last(&mut self, window: &mut Window, cx: &mut Context) -> Option { for (ix, item) in self.items.iter().enumerate().rev() { if item.is_selectable() { return self.select_index(ix, window, cx); } } None } fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context) { if self.select_last(window, cx).is_some() { cx.notify(); } } fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.selected_index { let next_index = ix + 1; if self.items.len() <= next_index { self.select_first(&SelectFirst, window, cx); } else { for (ix, item) in self.items.iter().enumerate().skip(next_index) { if item.is_selectable() { self.select_index(ix, window, cx); cx.notify(); break; } } } } else { self.select_first(&SelectFirst, window, cx); } } pub fn select_previous( &mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context, ) { if let Some(ix) = self.selected_index { if ix == 0 { self.handle_select_last(&SelectLast, window, cx); } else { for (ix, item) in self.items.iter().enumerate().take(ix).rev() { if item.is_selectable() { self.select_index(ix, window, cx); cx.notify(); break; } } } } else { self.handle_select_last(&SelectLast, window, cx); } } fn select_index( &mut self, ix: usize, window: &mut Window, cx: &mut Context, ) -> Option { let context = self.action_context.as_ref(); self.documentation_aside = None; let item = self.items.get(ix)?; if item.is_selectable() { self.selected_index = Some(ix); if let ContextMenuItem::Entry(entry) = item { if let Some(callback) = &entry.documentation_aside { self.documentation_aside = Some((ix, callback.clone())); } if self.eager && !entry.disabled { (entry.handler)(context, window, cx) } } } Some(ix) } pub fn on_action_dispatch( &mut self, dispatched: &dyn Action, window: &mut Window, cx: &mut Context, ) { if self.clicked { cx.propagate(); return; } if let Some(ix) = self.items.iter().position(|item| { if let ContextMenuItem::Entry(ContextMenuEntry { action: Some(action), disabled: false, .. }) = item { action.partial_eq(dispatched) } else { false } }) { self.select_index(ix, window, cx); self.delayed = true; cx.notify(); let action = dispatched.boxed_clone(); cx.spawn_in(window, async move |this, cx| { cx.background_executor() .timer(Duration::from_millis(50)) .await; cx.update(|window, cx| { this.update(cx, |this, cx| { this.cancel(&menu::Cancel, window, cx); window.dispatch_action(action, cx); }) }) }) .detach_and_log_err(cx); } else { cx.propagate() } } pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self { self._on_blur_subscription = new_subscription; self } fn render_menu_item( &self, ix: usize, item: &ContextMenuItem, window: &mut Window, cx: &mut Context, ) -> impl IntoElement + use<> { match item { ContextMenuItem::Separator => ListSeparator.into_any_element(), ContextMenuItem::Header(header) => ListSubHeader::new(header.clone()) .inset(true) .into_any_element(), ContextMenuItem::HeaderWithLink(header, label, url) => { let url = url.clone(); let link_id = ElementId::Name(format!("link-{}", url).into()); ListSubHeader::new(header.clone()) .inset(true) .end_slot( Button::new(link_id, label.clone()) .color(Color::Muted) .label_size(LabelSize::Small) .size(ButtonSize::None) .style(ButtonStyle::Transparent) .on_click(move |_, _, cx| { let url = url.clone(); cx.open_url(&url); }) .into_any_element(), ) .into_any_element() } ContextMenuItem::Label(label) => ListItem::new(ix) .inset(true) .disabled(true) .child(Label::new(label.clone())) .into_any_element(), ContextMenuItem::Entry(entry) => self .render_menu_entry(ix, entry, window, cx) .into_any_element(), ContextMenuItem::CustomEntry { entry_render, handler, selectable, } => { let handler = handler.clone(); let menu = cx.entity().downgrade(); let selectable = *selectable; ListItem::new(ix) .inset(true) .toggle_state(if selectable { Some(ix) == self.selected_index } else { false }) .selectable(selectable) .when(selectable, |item| { item.on_click({ let context = self.action_context.clone(); let keep_open_on_confirm = self.keep_open_on_confirm; move |_, window, cx| { handler(context.as_ref(), window, cx); menu.update(cx, |menu, cx| { menu.clicked = true; if keep_open_on_confirm { menu.rebuild(window, cx); } else { cx.emit(DismissEvent); } }) .ok(); } }) }) .child(entry_render(window, cx)) .into_any_element() } } } fn render_menu_entry( &self, ix: usize, entry: &ContextMenuEntry, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let ContextMenuEntry { toggle, label, handler, icon, icon_position, icon_size, icon_color, action, disabled, documentation_aside, end_slot_icon, end_slot_title, end_slot_handler, show_end_slot_on_hover, } = entry; let this = cx.weak_entity(); let handler = handler.clone(); let menu = cx.entity().downgrade(); let icon_color = if *disabled { Color::Muted } else if toggle.is_some() { icon_color.unwrap_or(Color::Accent) } else { icon_color.unwrap_or(Color::Default) }; let label_color = if *disabled { Color::Disabled } else { Color::Default }; let label_element = if let Some(icon_name) = icon { h_flex() .gap_1p5() .when( *icon_position == IconPosition::Start && toggle.is_none(), |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)), ) .child(Label::new(label.clone()).color(label_color).truncate()) .when(*icon_position == IconPosition::End, |flex| { flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)) }) .into_any_element() } else { Label::new(label.clone()) .color(label_color) .truncate() .into_any_element() }; div() .id(("context-menu-child", ix)) .when_some(documentation_aside.clone(), |this, documentation_aside| { this.occlude() .on_hover(cx.listener(move |menu, hovered, _, cx| { if *hovered { menu.documentation_aside = Some((ix, documentation_aside.clone())); } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { menu.documentation_aside = None; } cx.notify(); })) }) .child( ListItem::new(ix) .group_name("label_container") .inset(true) .disabled(*disabled) .toggle_state(Some(ix) == self.selected_index) .when_some(*toggle, |list_item, (position, toggled)| { let contents = div() .flex_none() .child( Icon::new(icon.unwrap_or(IconName::Check)) .color(icon_color) .size(*icon_size), ) .when(!toggled, |contents| contents.invisible()); match position { IconPosition::Start => list_item.start_slot(contents), IconPosition::End => list_item.end_slot(contents), } }) .child( h_flex() .w_full() .justify_between() .child(label_element) .debug_selector(|| format!("MENU_ITEM-{}", label)) .children(action.as_ref().and_then(|action| { self.action_context .as_ref() .map(|focus| { KeyBinding::for_action_in(&**action, focus, window, cx) }) .unwrap_or_else(|| { KeyBinding::for_action(&**action, window, cx) }) .map(|binding| { div().ml_4().child(binding.disabled(*disabled)).when( *disabled && documentation_aside.is_some(), |parent| parent.invisible(), ) }) })) .when(*disabled && documentation_aside.is_some(), |parent| { parent.child( Icon::new(IconName::Info) .size(IconSize::XSmall) .color(Color::Muted), ) }), ) .when_some( end_slot_icon .as_ref() .zip(self.end_slot_action.as_ref()) .zip(end_slot_title.as_ref()) .zip(end_slot_handler.as_ref()), |el, (((icon, action), title), handler)| { el.end_slot({ let icon_button = IconButton::new("end-slot-icon", *icon) .shape(IconButtonShape::Square) .tooltip({ let action_context = self.action_context.clone(); let title = title.clone(); let action = action.boxed_clone(); move |window, cx| { action_context .as_ref() .map(|focus| { Tooltip::for_action_in( title.clone(), &*action, focus, window, cx, ) }) .unwrap_or_else(|| { Tooltip::for_action( title.clone(), &*action, window, cx, ) }) } }) .on_click({ let handler = handler.clone(); move |_, window, cx| { handler(None, window, cx); this.update(cx, |this, cx| { this.rebuild(window, cx); cx.notify(); }) .ok(); } }); if *show_end_slot_on_hover { div() .visible_on_hover("label_container") .child(icon_button) .into_any_element() } else { icon_button.into_any_element() } }) }, ) .on_click({ let context = self.action_context.clone(); let keep_open_on_confirm = self.keep_open_on_confirm; move |_, window, cx| { handler(context.as_ref(), window, cx); menu.update(cx, |menu, cx| { menu.clicked = true; if keep_open_on_confirm { menu.rebuild(window, cx); } else { cx.emit(DismissEvent); } }) .ok(); } }), ) .into_any_element() } } impl ContextMenuItem { fn is_selectable(&self) -> bool { match self { ContextMenuItem::Header(_) | ContextMenuItem::HeaderWithLink(_, _, _) | ContextMenuItem::Separator | ContextMenuItem::Label { .. } => false, ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled, ContextMenuItem::CustomEntry { selectable, .. } => *selectable, } } } impl Render for ContextMenu { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let window_size = window.viewport_size(); let rem_size = window.rem_size(); let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0; let aside = self.documentation_aside.clone(); let render_aside = |aside: DocumentationAside, cx: &mut Context| { WithRemSize::new(ui_font_size) .occlude() .elevation_2(cx) .p_2() .overflow_hidden() .when(is_wide_window, |this| this.max_w_96()) .when(!is_wide_window, |this| this.max_w_48()) .child((aside.render)(cx)) }; h_flex() .when(is_wide_window, |this| this.flex_row()) .when(!is_wide_window, |this| this.flex_col()) .w_full() .items_start() .gap_1() .child(div().children(aside.clone().and_then(|(_, aside)| { (aside.side == DocumentationSide::Left).then(|| render_aside(aside, cx)) }))) .child( WithRemSize::new(ui_font_size) .occlude() .elevation_2(cx) .flex() .flex_row() .child( v_flex() .id("context-menu") .max_h(vh(0.75, window)) .when_some(self.fixed_width, |this, width| { this.w(width).overflow_x_hidden() }) .when(self.fixed_width.is_none(), |this| { this.min_w(px(200.)).flex_1() }) .overflow_y_scroll() .track_focus(&self.focus_handle(cx)) .on_mouse_down_out(cx.listener(|this, _, window, cx| { this.cancel(&menu::Cancel, window, cx) })) .key_context(self.key_context.as_ref()) .on_action(cx.listener(ContextMenu::select_first)) .on_action(cx.listener(ContextMenu::handle_select_last)) .on_action(cx.listener(ContextMenu::select_next)) .on_action(cx.listener(ContextMenu::select_previous)) .on_action(cx.listener(ContextMenu::confirm)) .on_action(cx.listener(ContextMenu::cancel)) .when_some(self.end_slot_action.as_ref(), |el, action| { el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot)) }) .when(!self.delayed, |mut el| { for item in self.items.iter() { if let ContextMenuItem::Entry(ContextMenuEntry { action: Some(action), disabled: false, .. }) = item { el = el.on_boxed_action( &**action, cx.listener(ContextMenu::on_action_dispatch), ); } } el }) .child( List::new().children( self.items.iter().enumerate().map(|(ix, item)| { self.render_menu_item(ix, item, window, cx) }), ), ), ), ) .child(div().children(aside.and_then(|(_, aside)| { (aside.side == DocumentationSide::Right).then(|| render_aside(aside, cx)) }))) } }