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