1use std::rc::Rc;
2
3use gpui::{
4 anchored, deferred, div, point, prelude::*, px, AnchoredPositionMode, AnyElement, App,
5 IntoElement, KeyDownEvent, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
6 StyleRefinement, Styled, Window,
7};
8
9use crate::compat::StyleCompatExt;
10
11use crate::dropdown_menu::{
12 handle_menu_keydown, initial_highlight, render_panel, set_open, DropdownMenuEntry,
13 DropdownMenuState, MenuPanelContext, OpenChangeHandler,
14};
15use crate::motion::StyledSlot;
16
17#[derive(IntoElement)]
19pub struct ContextMenu {
20 id: SharedString,
21 controlled_open: Option<bool>,
22 default_open: bool,
23 position: Option<Point<Pixels>>,
24 entries: Vec<DropdownMenuEntry>,
25 on_open_change: Option<OpenChangeHandler>,
26 style: StyleRefinement,
27 children: Vec<AnyElement>,
28}
29
30impl ContextMenu {
31 pub fn new(id: impl Into<SharedString>) -> Self {
32 Self {
33 id: id.into(),
34 controlled_open: None,
35 default_open: false,
36 position: None,
37 entries: Vec::new(),
38 on_open_change: None,
39 style: StyleRefinement::default(),
40 children: Vec::new(),
41 }
42 }
43
44 pub fn open(mut self, open: bool) -> Self {
45 self.controlled_open = Some(open);
46 self
47 }
48
49 pub fn default_open(mut self, open: bool) -> Self {
50 self.default_open = open;
51 self
52 }
53
54 pub fn position(mut self, origin: Point<Pixels>) -> Self {
56 self.position = Some(origin);
57 self
58 }
59
60 pub fn entries(mut self, entries: impl IntoIterator<Item = DropdownMenuEntry>) -> Self {
61 self.entries = entries.into_iter().collect();
62 self
63 }
64
65 pub fn on_open_change(
66 mut self,
67 listener: impl Fn(bool, &mut Window, &mut App) + 'static,
68 ) -> Self {
69 self.on_open_change = Some(Rc::new(listener));
70 self
71 }
72}
73
74impl Styled for ContextMenu {
75 fn style(&mut self) -> &mut StyleRefinement {
76 &mut self.style
77 }
78}
79
80impl ParentElement for ContextMenu {
81 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
82 self.children.extend(elements);
83 }
84}
85
86impl RenderOnce for ContextMenu {
87 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
88 let initial_open = self.controlled_open.unwrap_or(self.default_open);
89 let initial_entries = self.entries.clone();
90 let initial_origin = self.position.unwrap_or_else(|| point(px(0.), px(0.)));
91 let state = window.use_keyed_state(self.id.clone(), cx, move |_, cx| DropdownMenuState {
92 focus_handle: cx.focus_handle().tab_stop(true),
93 open: initial_open,
94 highlighted: initial_open
95 .then(|| initial_highlight(&initial_entries))
96 .flatten(),
97 submenu: None,
98 origin: initial_origin,
99 origin_window: false,
100 previous_focus: None,
101 });
102
103 if let Some(controlled_open) = self.controlled_open {
104 if state.read(cx).open != controlled_open {
105 state.update(cx, |menu, _| {
106 menu.open = controlled_open;
107 menu.highlighted = controlled_open
108 .then(|| initial_highlight(&self.entries))
109 .flatten();
110 menu.submenu = None;
111 });
112 }
113 }
114 if let Some(position) = self.position {
115 if state.read(cx).origin != position || state.read(cx).origin_window {
116 state.update(cx, |menu, _| {
117 menu.origin = position;
118 menu.origin_window = false;
119 });
120 }
121 }
122
123 let open = state.read(cx).open;
124 let highlighted = state.read(cx).highlighted;
125 let submenu = state.read(cx).submenu;
126 let origin = state.read(cx).origin;
127 let origin_window = state.read(cx).origin_window;
128 let focus_handle = state.read(cx).focus_handle.clone();
129 let restore_focus = state
130 .read(cx)
131 .previous_focus
132 .clone()
133 .unwrap_or_else(|| focus_handle.clone());
134
135 let user_change = self.on_open_change.clone();
136 let restore_state = state.clone();
137 let on_open_change: OpenChangeHandler = Rc::new(move |open, window, cx| {
138 if !open {
139 let previous = restore_state.read(cx).previous_focus.clone();
140 if let Some(previous) = previous {
141 previous.focus(window);
142 }
143 }
144 if let Some(user_change) = &user_change {
145 user_change(open, window, cx);
146 }
147 });
148
149 let click_state = state.clone();
150 let click_entries = self.entries.clone();
151 let click_change = on_open_change.clone();
152 let click_focus = focus_handle.clone();
153 let keyboard_state = state.clone();
154 let keyboard_entries = self.entries.clone();
155 let keyboard_change = on_open_change.clone();
156 let keyboard_restore = restore_focus.clone();
157
158 let target = div()
159 .id(SharedString::from(format!("{}-target", self.id)))
160 .debug_selector({
161 let selector = format!("{}-target", self.id);
162 move || selector.clone()
163 })
164 .relative()
165 .flex()
166 .flex_none()
167 .self_start()
168 .refine_style(&self.style)
169 .on_mouse_down(MouseButton::Right, move |event, window, cx| {
170 let previous = window.focused(cx);
171 click_state.update(cx, |menu, cx| {
172 menu.origin = event.position;
173 menu.origin_window = true;
174 menu.previous_focus = previous;
175 cx.notify();
176 });
177 set_open(
178 &click_state,
179 true,
180 &click_entries,
181 Some(&click_change),
182 window,
183 cx,
184 );
185 click_focus.focus(window);
186 cx.stop_propagation();
187 })
188 .children(self.children);
189
190 let panel_context = MenuPanelContext {
191 state: state.clone(),
192 root_entries: self.entries.clone(),
193 focus_handle: focus_handle.clone(),
194 on_open_change: Some(on_open_change.clone()),
195 };
196 let panel = render_panel(
197 self.id.clone(),
198 self.entries,
199 highlighted,
200 submenu.map(|submenu| submenu.parent),
201 panel_context,
202 submenu.is_none(),
203 cx,
204 );
205
206 let menu = deferred(
207 anchored()
208 .anchor(gpui::Corner::TopLeft)
209 .position(origin)
210 .position_mode(if origin_window {
211 AnchoredPositionMode::Window
212 } else {
213 AnchoredPositionMode::Local
214 })
215 .snap_to_window_with_margin(px(8.))
216 .child(
217 div()
218 .track_focus(&focus_handle)
219 .on_key_down(move |event: &KeyDownEvent, window, cx| {
220 handle_menu_keydown(
221 event,
222 &keyboard_state,
223 &keyboard_entries,
224 Some(&keyboard_change),
225 &keyboard_restore,
226 window,
227 cx,
228 );
229 })
230 .child(panel),
231 ),
232 )
233 .with_priority(2);
234
235 target.when(open, |target| target.child(menu))
236 }
237}