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