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