use gpui::prelude::FluentBuilder as _; use gpui::{ App, ElementId, InteractiveElement as _, IntoElement, MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, SharedString, Window, div, }; use crate::menu::{Menu, MenuItem}; /// Builds the element that opens the menu on right-click. type TargetFn = Box gpui::AnyElement>; /// Builds the menu's items fresh each render. type ItemsFn = Box Vec>; /// A menu that opens at the pointer when its target is right-clicked. #[derive(IntoElement)] pub struct ContextMenu { id: ElementId, target: Option, items: Option, a11y_label: Option, } impl ContextMenu { /// Creates a context menu over a target set with [`ContextMenu::target`]. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), target: None, items: None, a11y_label: None, } } /// Sets the element that opens the menu on right-click. pub fn target(mut self, target: F) -> Self where F: Fn(&mut Window, &mut App) -> gpui::AnyElement + 'static, { self.target = Some(Box::new(target)); self } /// Sets the menu's items, built fresh each render. pub fn items(mut self, items: F) -> Self where F: Fn(&mut Window, &mut App) -> Vec + 'static, { self.items = Some(Box::new(items)); self } /// Overrides the menu's accessibility label. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } } impl RenderOnce for ContextMenu { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let open = window.with_element_namespace(self.id.clone(), |window| { window .use_keyed_state("context-open", cx, |_, _| false) .clone() }); let at = window.with_element_namespace(self.id.clone(), |window| { window .use_keyed_state("context-position", cx, |_, _| None) .clone() }); let is_open = *open.read(cx); let position = *at.read(cx); let target = self.target; let at_for_target = at.clone(); let trigger = move |window: &mut Window, cx: &mut App| { let base = target .as_ref() .map(|build| build(window, cx)) .unwrap_or_else(|| div().into_any_element()); div() .on_mouse_down(MouseButton::Right, { let at = at_for_target.clone(); move |event: &MouseDownEvent, _window, cx| { at.update(cx, |at, _| *at = Some(event.position)); } }) .child(base) .into_any_element() }; let items = move |window: &mut Window, cx: &mut App| { self.items .as_ref() .map(|build| build(window, cx)) .unwrap_or_default() }; Menu::new(self.id) .open(is_open) .on_change(move |next, _window, cx| open.update(cx, |open, _| *open = next)) .open_at(position) .trigger_button(MouseButton::Right) .trigger(trigger) .items(items) .when_some(self.a11y_label.clone(), |menu, label| { menu.accessibility_label(label) }) } }