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