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