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