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