1use std::rc::Rc;
2
3use gpui_kit::{
4 AnyElement, App, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, Length,
5 MouseButton, ParentElement, RenderOnce, StyleRefinement, Styled, Window,
6 base::{StyledExt as _, TestSupportExt as _},
7 div,
8};
9
10use super::{MenuAnchor, MenuEntry, MenuState, dropdown::EntriesBuilder, panel::MenuPanels};
11
12#[derive(IntoElement)]
34pub struct ContextMenu {
35 id: ElementId,
36 state: Entity<MenuState>,
37 items: Option<EntriesBuilder>,
38 action_context: Option<FocusHandle>,
39 menu_width: Option<Length>,
40 style: StyleRefinement,
41 children: Vec<AnyElement>,
42}
43
44impl ContextMenu {
45 pub fn new(id: impl Into<ElementId>, state: &Entity<MenuState>) -> Self {
47 Self {
48 id: id.into(),
49 state: state.clone(),
50 items: None,
51 action_context: None,
52 menu_width: None,
53 style: StyleRefinement::default(),
54 children: Vec::new(),
55 }
56 }
57
58 pub fn items(
60 mut self,
61 items: impl Fn(&mut Window, &mut App) -> Vec<MenuEntry> + 'static,
62 ) -> Self {
63 self.items = Some(Rc::new(items));
64 self
65 }
66
67 pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
72 self.action_context = Some(focus_handle.clone());
73 self
74 }
75
76 pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
79 self.menu_width = Some(width.into());
80 self
81 }
82}
83
84impl Styled for ContextMenu {
85 fn style(&mut self) -> &mut StyleRefinement {
86 &mut self.style
87 }
88}
89
90impl ParentElement for ContextMenu {
91 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
92 self.children.extend(elements);
93 }
94}
95
96impl RenderOnce for ContextMenu {
97 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
98 let state = self.state;
99 let items = self.items;
100 let context = self.action_context;
101 div()
102 .id(self.id.clone())
103 .test_support()
104 .refine_style(&self.style)
105 .on_mouse_down(MouseButton::Right, {
106 let state = state.clone();
107 move |event, window, cx| {
108 cx.stop_propagation();
109 let entries = items
110 .as_ref()
111 .map(|items| items(window, cx))
112 .unwrap_or_default();
113 if entries.is_empty() {
114 return;
115 }
116 let anchor = MenuAnchor::Point(event.position);
117 state.update(cx, |state, cx| match &context {
118 Some(context) => state.open_for(entries, anchor, context, window, cx),
119 None => state.open(entries, anchor, window, cx),
120 });
121 }
122 })
123 .children(self.children)
124 .child(MenuPanels::new(self.id, &state).width(self.menu_width))
125 }
126}