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