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