1use crate::{
2 h_stack, prelude::*, v_stack, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator,
3 ListSubHeader,
4};
5use gpui::{
6 px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
7 IntoElement, Render, Subscription, View, VisualContext,
8};
9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
10use std::{rc::Rc, time::Duration};
11
12enum ContextMenuItem {
13 Separator,
14 Header(SharedString),
15 Entry {
16 label: SharedString,
17 icon: Option<IconName>,
18 handler: Rc<dyn Fn(&mut WindowContext)>,
19 action: Option<Box<dyn Action>>,
20 },
21 CustomEntry {
22 entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
23 handler: Rc<dyn Fn(&mut WindowContext)>,
24 },
25}
26
27pub struct ContextMenu {
28 items: Vec<ContextMenuItem>,
29 focus_handle: FocusHandle,
30 selected_index: Option<usize>,
31 delayed: bool,
32 clicked: bool,
33 _on_blur_subscription: Subscription,
34}
35
36impl FocusableView for ContextMenu {
37 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
38 self.focus_handle.clone()
39 }
40}
41
42impl EventEmitter<DismissEvent> for ContextMenu {}
43
44impl ContextMenu {
45 pub fn build(
46 cx: &mut WindowContext,
47 f: impl FnOnce(Self, &mut WindowContext) -> Self,
48 ) -> View<Self> {
49 cx.new_view(|cx| {
50 let focus_handle = cx.focus_handle();
51 let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut ContextMenu, cx| {
52 this.cancel(&menu::Cancel, cx)
53 });
54 f(
55 Self {
56 items: Default::default(),
57 focus_handle,
58 selected_index: None,
59 delayed: false,
60 clicked: false,
61 _on_blur_subscription,
62 },
63 cx,
64 )
65 })
66 }
67
68 pub fn header(mut self, title: impl Into<SharedString>) -> Self {
69 self.items.push(ContextMenuItem::Header(title.into()));
70 self
71 }
72
73 pub fn separator(mut self) -> Self {
74 self.items.push(ContextMenuItem::Separator);
75 self
76 }
77
78 pub fn entry(
79 mut self,
80 label: impl Into<SharedString>,
81 action: Option<Box<dyn Action>>,
82 handler: impl Fn(&mut WindowContext) + 'static,
83 ) -> Self {
84 self.items.push(ContextMenuItem::Entry {
85 label: label.into(),
86 handler: Rc::new(handler),
87 icon: None,
88 action,
89 });
90 self
91 }
92
93 pub fn custom_entry(
94 mut self,
95 entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static,
96 handler: impl Fn(&mut WindowContext) + 'static,
97 ) -> Self {
98 self.items.push(ContextMenuItem::CustomEntry {
99 entry_render: Box::new(entry_render),
100 handler: Rc::new(handler),
101 });
102 self
103 }
104
105 pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
106 self.items.push(ContextMenuItem::Entry {
107 label: label.into(),
108 action: Some(action.boxed_clone()),
109 handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
110 icon: None,
111 });
112 self
113 }
114
115 pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
116 self.items.push(ContextMenuItem::Entry {
117 label: label.into(),
118 action: Some(action.boxed_clone()),
119 handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
120 icon: Some(IconName::Link),
121 });
122 self
123 }
124
125 pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
126 match self.selected_index.and_then(|ix| self.items.get(ix)) {
127 Some(
128 ContextMenuItem::Entry { handler, .. }
129 | ContextMenuItem::CustomEntry { handler, .. },
130 ) => (handler)(cx),
131 _ => {}
132 }
133
134 cx.emit(DismissEvent);
135 }
136
137 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
138 cx.emit(DismissEvent);
139 cx.emit(DismissEvent);
140 }
141
142 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
143 self.selected_index = self.items.iter().position(|item| item.is_selectable());
144 cx.notify();
145 }
146
147 pub fn select_last(&mut self) -> Option<usize> {
148 for (ix, item) in self.items.iter().enumerate().rev() {
149 if item.is_selectable() {
150 self.selected_index = Some(ix);
151 return Some(ix);
152 }
153 }
154 None
155 }
156
157 fn handle_select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
158 if self.select_last().is_some() {
159 cx.notify();
160 }
161 }
162
163 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
164 if let Some(ix) = self.selected_index {
165 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
166 if item.is_selectable() {
167 self.selected_index = Some(ix);
168 cx.notify();
169 break;
170 }
171 }
172 } else {
173 self.select_first(&Default::default(), cx);
174 }
175 }
176
177 pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
178 if let Some(ix) = self.selected_index {
179 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
180 if item.is_selectable() {
181 self.selected_index = Some(ix);
182 cx.notify();
183 break;
184 }
185 }
186 } else {
187 self.handle_select_last(&Default::default(), cx);
188 }
189 }
190
191 pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
192 if self.clicked {
193 cx.propagate();
194 return;
195 }
196
197 if let Some(ix) = self.items.iter().position(|item| {
198 if let ContextMenuItem::Entry {
199 action: Some(action),
200 ..
201 } = item
202 {
203 action.partial_eq(&**dispatched)
204 } else {
205 false
206 }
207 }) {
208 self.selected_index = Some(ix);
209 self.delayed = true;
210 cx.notify();
211 let action = dispatched.boxed_clone();
212 cx.spawn(|this, mut cx| async move {
213 cx.background_executor()
214 .timer(Duration::from_millis(50))
215 .await;
216 this.update(&mut cx, |this, cx| {
217 cx.dispatch_action(action);
218 this.cancel(&menu::Cancel, cx)
219 })
220 })
221 .detach_and_log_err(cx);
222 } else {
223 cx.propagate()
224 }
225 }
226}
227
228impl ContextMenuItem {
229 fn is_selectable(&self) -> bool {
230 matches!(self, Self::Entry { .. } | Self::CustomEntry { .. })
231 }
232}
233
234impl Render for ContextMenu {
235 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
236 div().elevation_2(cx).flex().flex_row().child(
237 v_stack()
238 .min_w(px(200.))
239 .track_focus(&self.focus_handle)
240 .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
241 .key_context("menu")
242 .on_action(cx.listener(ContextMenu::select_first))
243 .on_action(cx.listener(ContextMenu::handle_select_last))
244 .on_action(cx.listener(ContextMenu::select_next))
245 .on_action(cx.listener(ContextMenu::select_prev))
246 .on_action(cx.listener(ContextMenu::confirm))
247 .on_action(cx.listener(ContextMenu::cancel))
248 .when(!self.delayed, |mut el| {
249 for item in self.items.iter() {
250 if let ContextMenuItem::Entry {
251 action: Some(action),
252 ..
253 } = item
254 {
255 el = el.on_boxed_action(
256 &**action,
257 cx.listener(ContextMenu::on_action_dispatch),
258 );
259 }
260 }
261 el
262 })
263 .flex_none()
264 .child(List::new().children(self.items.iter_mut().enumerate().map(
265 |(ix, item)| match item {
266 ContextMenuItem::Separator => ListSeparator.into_any_element(),
267 ContextMenuItem::Header(header) => {
268 ListSubHeader::new(header.clone()).into_any_element()
269 }
270 ContextMenuItem::Entry {
271 label,
272 handler,
273 icon,
274 action,
275 } => {
276 let handler = handler.clone();
277 let menu = cx.view().downgrade();
278
279 let label_element = if let Some(icon) = icon {
280 h_stack()
281 .gap_1()
282 .child(Label::new(label.clone()))
283 .child(Icon::new(*icon))
284 .into_any_element()
285 } else {
286 Label::new(label.clone()).into_any_element()
287 };
288
289 ListItem::new(ix)
290 .inset(true)
291 .selected(Some(ix) == self.selected_index)
292 .on_click(move |_, cx| {
293 handler(cx);
294 menu.update(cx, |menu, cx| {
295 menu.clicked = true;
296 cx.emit(DismissEvent);
297 })
298 .ok();
299 })
300 .child(
301 h_stack()
302 .w_full()
303 .justify_between()
304 .child(label_element)
305 .children(action.as_ref().and_then(|action| {
306 KeyBinding::for_action(&**action, cx)
307 .map(|binding| div().ml_1().child(binding))
308 })),
309 )
310 .into_any_element()
311 }
312 ContextMenuItem::CustomEntry {
313 entry_render,
314 handler,
315 } => {
316 let handler = handler.clone();
317 let menu = cx.view().downgrade();
318 ListItem::new(ix)
319 .inset(true)
320 .selected(Some(ix) == self.selected_index)
321 .on_click(move |_, cx| {
322 handler(cx);
323 menu.update(cx, |menu, cx| {
324 menu.clicked = true;
325 cx.emit(DismissEvent);
326 })
327 .ok();
328 })
329 .child(entry_render(cx))
330 .into_any_element()
331 }
332 },
333 ))),
334 )
335 }
336}