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