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