1use gpui::{
2 elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
3 Action, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
4 SizeConstraint, Subscription, View, ViewContext,
5};
6use menu::*;
7use settings::Settings;
8use std::{any::TypeId, time::Duration};
9
10#[derive(Copy, Clone, PartialEq)]
11struct Clicked;
12
13impl_internal_actions!(context_menu, [Clicked]);
14
15pub fn init(cx: &mut MutableAppContext) {
16 cx.add_action(ContextMenu::select_first);
17 cx.add_action(ContextMenu::select_last);
18 cx.add_action(ContextMenu::select_next);
19 cx.add_action(ContextMenu::select_prev);
20 cx.add_action(ContextMenu::clicked);
21 cx.add_action(ContextMenu::confirm);
22 cx.add_action(ContextMenu::cancel);
23}
24
25pub enum ContextMenuItem {
26 Item {
27 label: String,
28 action: Box<dyn Action>,
29 icon: Option<String>,
30 },
31 Separator,
32}
33
34impl ContextMenuItem {
35 pub fn item(label: impl ToString, icon: Option<&str>, action: impl 'static + Action) -> Self {
36 Self::Item {
37 label: label.to_string(),
38 action: Box::new(action),
39 icon: icon.map(|item| item.to_string()),
40 }
41 }
42
43 pub fn separator() -> Self {
44 Self::Separator
45 }
46
47 fn is_separator(&self) -> bool {
48 matches!(self, Self::Separator)
49 }
50
51 fn action_id(&self) -> Option<TypeId> {
52 match self {
53 ContextMenuItem::Item { action, .. } => Some(action.id()),
54 ContextMenuItem::Separator => None,
55 }
56 }
57}
58
59pub struct ContextMenu {
60 show_count: usize,
61 position: Vector2F,
62 items: Vec<ContextMenuItem>,
63 selected_index: Option<usize>,
64 visible: bool,
65 previously_focused_view_id: Option<usize>,
66 clicked: bool,
67 _actions_observation: Subscription,
68}
69
70impl Entity for ContextMenu {
71 type Event = ();
72}
73
74impl View for ContextMenu {
75 fn ui_name() -> &'static str {
76 "ContextMenu"
77 }
78
79 fn keymap_context(&self, _: &AppContext) -> keymap::Context {
80 let mut cx = Self::default_keymap_context();
81 cx.set.insert("menu".into());
82 cx
83 }
84
85 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
86 if !self.visible {
87 return Empty::new().boxed();
88 }
89
90 // Render the menu once at minimum width.
91 let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
92 let expanded_menu = self
93 .render_menu(cx)
94 .constrained()
95 .dynamically(move |constraint, cx| {
96 SizeConstraint::strict_along(
97 Axis::Horizontal,
98 collapsed_menu.layout(constraint, cx).x(),
99 )
100 })
101 .boxed();
102
103 Overlay::new(expanded_menu)
104 .hoverable(true)
105 .fit_mode(OverlayFitMode::SnapToWindow)
106 .with_abs_position(self.position)
107 .boxed()
108 }
109
110 fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
111 self.reset(cx);
112 }
113}
114
115impl ContextMenu {
116 pub fn new(cx: &mut ViewContext<Self>) -> Self {
117 Self {
118 show_count: 0,
119 position: Default::default(),
120 items: Default::default(),
121 selected_index: Default::default(),
122 visible: Default::default(),
123 previously_focused_view_id: Default::default(),
124 clicked: false,
125 _actions_observation: cx.observe_actions(Self::action_dispatched),
126 }
127 }
128
129 pub fn visible(&self) -> bool {
130 self.visible
131 }
132
133 fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
134 if let Some(ix) = self
135 .items
136 .iter()
137 .position(|item| item.action_id() == Some(action_id))
138 {
139 if self.clicked {
140 self.cancel(&Default::default(), cx);
141 } else {
142 self.selected_index = Some(ix);
143 cx.notify();
144 cx.spawn(|this, mut cx| async move {
145 cx.background().timer(Duration::from_millis(50)).await;
146 this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
147 })
148 .detach();
149 }
150 }
151 }
152
153 fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
154 self.clicked = true;
155 }
156
157 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
158 if let Some(ix) = self.selected_index {
159 if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
160 let window_id = cx.window_id();
161 let view_id = cx.view_id();
162 cx.dispatch_action_at(window_id, view_id, action.as_ref());
163 self.reset(cx);
164 }
165 }
166 }
167
168 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
169 self.reset(cx);
170 let show_count = self.show_count;
171 cx.defer(move |this, cx| {
172 if cx.handle().is_focused(cx) && this.show_count == show_count {
173 let window_id = cx.window_id();
174 (**cx).focus(window_id, this.previously_focused_view_id.take());
175 }
176 });
177 }
178
179 fn reset(&mut self, cx: &mut ViewContext<Self>) {
180 self.items.clear();
181 self.visible = false;
182 self.selected_index.take();
183 self.clicked = false;
184 cx.notify();
185 }
186
187 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
188 self.selected_index = self.items.iter().position(|item| !item.is_separator());
189 cx.notify();
190 }
191
192 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
193 for (ix, item) in self.items.iter().enumerate().rev() {
194 if !item.is_separator() {
195 self.selected_index = Some(ix);
196 cx.notify();
197 break;
198 }
199 }
200 }
201
202 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
203 if let Some(ix) = self.selected_index {
204 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
205 if !item.is_separator() {
206 self.selected_index = Some(ix);
207 cx.notify();
208 break;
209 }
210 }
211 } else {
212 self.select_first(&Default::default(), cx);
213 }
214 }
215
216 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
217 if let Some(ix) = self.selected_index {
218 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
219 if !item.is_separator() {
220 self.selected_index = Some(ix);
221 cx.notify();
222 break;
223 }
224 }
225 } else {
226 self.select_last(&Default::default(), cx);
227 }
228 }
229
230 pub fn show(
231 &mut self,
232 position: Vector2F,
233 items: impl IntoIterator<Item = ContextMenuItem>,
234 cx: &mut ViewContext<Self>,
235 ) {
236 let mut items = items.into_iter().peekable();
237 if items.peek().is_some() {
238 self.items = items.collect();
239 self.position = position;
240 self.visible = true;
241 self.show_count += 1;
242 if !cx.is_self_focused() {
243 self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
244 }
245 cx.focus_self();
246 } else {
247 self.visible = false;
248 }
249 cx.notify();
250 }
251
252 fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
253 let style = cx.global::<Settings>().theme.context_menu.clone();
254 Flex::row()
255 .with_child(
256 Flex::column()
257 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
258 match item {
259 ContextMenuItem::Item { label, icon, .. } => {
260 let style = style
261 .item
262 .style_for(Default::default(), Some(ix) == self.selected_index);
263 let mut line = Flex::row();
264 if let Some(_) = icon {
265 line.add_child(
266 Empty::new()
267 .constrained()
268 .with_width(style.icon_width)
269 .boxed(),
270 );
271 }
272 line.add_child(
273 Label::new(label.to_string(), style.label.clone())
274 .contained()
275 .with_style(style.container)
276 .with_margin_left(if icon.is_some() {
277 style.icon_spacing
278 } else {
279 0.
280 })
281 .boxed(),
282 );
283 line.boxed()
284 }
285 ContextMenuItem::Separator => Empty::new()
286 .collapsed()
287 .contained()
288 .with_style(style.separator)
289 .constrained()
290 .with_height(1.)
291 .boxed(),
292 }
293 }))
294 .boxed(),
295 )
296 .with_child(
297 Flex::column()
298 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
299 match item {
300 ContextMenuItem::Item { action, .. } => {
301 let style = style
302 .item
303 .style_for(Default::default(), Some(ix) == self.selected_index);
304 KeystrokeLabel::new(
305 action.boxed_clone(),
306 style.keystroke.container,
307 style.keystroke.text.clone(),
308 )
309 .boxed()
310 }
311 ContextMenuItem::Separator => Empty::new()
312 .collapsed()
313 .constrained()
314 .with_height(1.)
315 .contained()
316 .with_style(style.separator)
317 .boxed(),
318 }
319 }))
320 .contained()
321 .with_margin_left(style.keystroke_margin)
322 .boxed(),
323 )
324 .contained()
325 .with_style(style.container)
326 }
327
328 fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
329 enum Menu {}
330 enum MenuItem {}
331 let style = cx.global::<Settings>().theme.context_menu.clone();
332 MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
333 Flex::column()
334 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
335 match item {
336 ContextMenuItem::Item {
337 label,
338 action,
339 icon,
340 } => {
341 let action = action.boxed_clone();
342 MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
343 let style =
344 style.item.style_for(state, Some(ix) == self.selected_index);
345
346 let mut line = Flex::row();
347 if let Some(icon_file) = icon {
348 line.add_child(
349 Svg::new(format!("icons/{}", icon_file))
350 .with_color(style.label.color)
351 .constrained()
352 .with_width(style.icon_width)
353 .aligned()
354 .boxed(),
355 )
356 }
357
358 line.with_child(
359 Label::new(label.to_string(), style.label.clone())
360 .contained()
361 .with_margin_left(if icon.is_some() {
362 style.icon_spacing
363 } else {
364 0.
365 })
366 .boxed(),
367 )
368 .with_child({
369 KeystrokeLabel::new(
370 action.boxed_clone(),
371 style.keystroke.container,
372 style.keystroke.text.clone(),
373 )
374 .flex_float()
375 .boxed()
376 })
377 .contained()
378 .with_style(style.container)
379 .boxed()
380 })
381 .with_cursor_style(CursorStyle::PointingHand)
382 .on_click(MouseButton::Left, move |_, cx| {
383 cx.dispatch_action(Clicked);
384 cx.dispatch_any_action(action.boxed_clone());
385 })
386 .boxed()
387 }
388 ContextMenuItem::Separator => Empty::new()
389 .constrained()
390 .with_height(1.)
391 .contained()
392 .with_style(style.separator)
393 .boxed(),
394 }
395 }))
396 .contained()
397 .with_style(style.container)
398 .boxed()
399 })
400 .on_down_out(MouseButton::Left, |_, cx| cx.dispatch_action(Cancel))
401 .on_down_out(MouseButton::Right, |_, cx| cx.dispatch_action(Cancel))
402 }
403}