1use gpui::{
2 elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
3 platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
4 MutableAppContext, RenderContext, 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 },
30 Separator,
31}
32
33impl ContextMenuItem {
34 pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
35 Self::Item {
36 label: label.to_string(),
37 action: Box::new(action),
38 }
39 }
40
41 pub fn separator() -> Self {
42 Self::Separator
43 }
44
45 fn is_separator(&self) -> bool {
46 matches!(self, Self::Separator)
47 }
48
49 fn action_id(&self) -> Option<TypeId> {
50 match self {
51 ContextMenuItem::Item { action, .. } => Some(action.id()),
52 ContextMenuItem::Separator => None,
53 }
54 }
55}
56
57pub struct ContextMenu {
58 show_count: usize,
59 anchor_position: Vector2F,
60 anchor_corner: AnchorCorner,
61 items: Vec<ContextMenuItem>,
62 selected_index: Option<usize>,
63 visible: bool,
64 previously_focused_view_id: Option<usize>,
65 clicked: bool,
66 parent_view_id: usize,
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) -> KeymapContext {
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 .with_hoverable(true)
105 .with_fit_mode(OverlayFitMode::SnapToWindow)
106 .with_anchor_position(self.anchor_position)
107 .with_anchor_corner(self.anchor_corner)
108 .boxed()
109 }
110
111 fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
112 self.reset(cx);
113 }
114}
115
116impl ContextMenu {
117 pub fn new(cx: &mut ViewContext<Self>) -> Self {
118 let parent_view_id = cx.parent().unwrap();
119
120 Self {
121 show_count: 0,
122 anchor_position: Default::default(),
123 anchor_corner: AnchorCorner::TopLeft,
124 items: Default::default(),
125 selected_index: Default::default(),
126 visible: Default::default(),
127 previously_focused_view_id: Default::default(),
128 clicked: false,
129 parent_view_id,
130 _actions_observation: cx.observe_actions(Self::action_dispatched),
131 }
132 }
133
134 pub fn visible(&self) -> bool {
135 self.visible
136 }
137
138 fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
139 if let Some(ix) = self
140 .items
141 .iter()
142 .position(|item| item.action_id() == Some(action_id))
143 {
144 if self.clicked {
145 self.cancel(&Default::default(), cx);
146 } else {
147 self.selected_index = Some(ix);
148 cx.notify();
149 cx.spawn(|this, mut cx| async move {
150 cx.background().timer(Duration::from_millis(50)).await;
151 this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
152 })
153 .detach();
154 }
155 }
156 }
157
158 fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
159 self.clicked = true;
160 }
161
162 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
163 if let Some(ix) = self.selected_index {
164 if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
165 cx.dispatch_any_action(action.boxed_clone());
166 self.reset(cx);
167 }
168 }
169 }
170
171 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
172 self.reset(cx);
173 let show_count = self.show_count;
174 cx.defer(move |this, cx| {
175 if cx.handle().is_focused(cx) && this.show_count == show_count {
176 let window_id = cx.window_id();
177 (**cx).focus(window_id, this.previously_focused_view_id.take());
178 }
179 });
180 }
181
182 fn reset(&mut self, cx: &mut ViewContext<Self>) {
183 self.items.clear();
184 self.visible = false;
185 self.selected_index.take();
186 self.clicked = false;
187 cx.notify();
188 }
189
190 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
191 self.selected_index = self.items.iter().position(|item| !item.is_separator());
192 cx.notify();
193 }
194
195 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
196 for (ix, item) in self.items.iter().enumerate().rev() {
197 if !item.is_separator() {
198 self.selected_index = Some(ix);
199 cx.notify();
200 break;
201 }
202 }
203 }
204
205 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
206 if let Some(ix) = self.selected_index {
207 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
208 if !item.is_separator() {
209 self.selected_index = Some(ix);
210 cx.notify();
211 break;
212 }
213 }
214 } else {
215 self.select_first(&Default::default(), cx);
216 }
217 }
218
219 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
220 if let Some(ix) = self.selected_index {
221 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
222 if !item.is_separator() {
223 self.selected_index = Some(ix);
224 cx.notify();
225 break;
226 }
227 }
228 } else {
229 self.select_last(&Default::default(), cx);
230 }
231 }
232
233 pub fn show(
234 &mut self,
235 anchor_position: Vector2F,
236 anchor_corner: AnchorCorner,
237 items: impl IntoIterator<Item = ContextMenuItem>,
238 cx: &mut ViewContext<Self>,
239 ) {
240 let mut items = items.into_iter().peekable();
241 if items.peek().is_some() {
242 self.items = items.collect();
243 self.anchor_position = anchor_position;
244 self.anchor_corner = anchor_corner;
245 self.visible = true;
246 self.show_count += 1;
247 if !cx.is_self_focused() {
248 self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
249 }
250 cx.focus_self();
251 } else {
252 self.visible = false;
253 }
254 cx.notify();
255 }
256
257 fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
258 let window_id = cx.window_id();
259 let style = cx.global::<Settings>().theme.context_menu.clone();
260 Flex::row()
261 .with_child(
262 Flex::column()
263 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
264 match item {
265 ContextMenuItem::Item { label, .. } => {
266 let style = style.item.style_for(
267 &mut Default::default(),
268 Some(ix) == self.selected_index,
269 );
270
271 Label::new(label.to_string(), style.label.clone())
272 .contained()
273 .with_style(style.container)
274 .boxed()
275 }
276 ContextMenuItem::Separator => Empty::new()
277 .collapsed()
278 .contained()
279 .with_style(style.separator)
280 .constrained()
281 .with_height(1.)
282 .boxed(),
283 }
284 }))
285 .boxed(),
286 )
287 .with_child(
288 Flex::column()
289 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
290 match item {
291 ContextMenuItem::Item { action, .. } => {
292 let style = style.item.style_for(
293 &mut Default::default(),
294 Some(ix) == self.selected_index,
295 );
296 KeystrokeLabel::new(
297 window_id,
298 self.parent_view_id,
299 action.boxed_clone(),
300 style.keystroke.container,
301 style.keystroke.text.clone(),
302 )
303 .boxed()
304 }
305 ContextMenuItem::Separator => Empty::new()
306 .collapsed()
307 .constrained()
308 .with_height(1.)
309 .contained()
310 .with_style(style.separator)
311 .boxed(),
312 }
313 }))
314 .contained()
315 .with_margin_left(style.keystroke_margin)
316 .boxed(),
317 )
318 .contained()
319 .with_style(style.container)
320 }
321
322 fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
323 enum Menu {}
324 enum MenuItem {}
325
326 let style = cx.global::<Settings>().theme.context_menu.clone();
327
328 let window_id = cx.window_id();
329 MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
330 Flex::column()
331 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
332 match item {
333 ContextMenuItem::Item { label, action } => {
334 let action = action.boxed_clone();
335
336 MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
337 let style =
338 style.item.style_for(state, Some(ix) == self.selected_index);
339
340 Flex::row()
341 .with_child(
342 Label::new(label.to_string(), style.label.clone())
343 .contained()
344 .boxed(),
345 )
346 .with_child({
347 KeystrokeLabel::new(
348 window_id,
349 self.parent_view_id,
350 action.boxed_clone(),
351 style.keystroke.container,
352 style.keystroke.text.clone(),
353 )
354 .flex_float()
355 .boxed()
356 })
357 .contained()
358 .with_style(style.container)
359 .boxed()
360 })
361 .with_cursor_style(CursorStyle::PointingHand)
362 .on_click(MouseButton::Left, move |_, cx| {
363 cx.dispatch_action(Clicked);
364 cx.dispatch_any_action(action.boxed_clone());
365 })
366 .on_drag(MouseButton::Left, |_, _| {})
367 .boxed()
368 }
369 ContextMenuItem::Separator => Empty::new()
370 .constrained()
371 .with_height(1.)
372 .contained()
373 .with_style(style.separator)
374 .boxed(),
375 }
376 }))
377 .contained()
378 .with_style(style.container)
379 .boxed()
380 })
381 .on_down_out(MouseButton::Left, |_, cx| cx.dispatch_action(Cancel))
382 .on_down_out(MouseButton::Right, |_, cx| cx.dispatch_action(Cancel))
383 }
384}