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 .with_abs_position(self.position)
96 .boxed()
97 }
98
99 fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
100 self.reset(cx);
101 }
102}
103
104impl ContextMenu {
105 pub fn new(cx: &mut ViewContext<Self>) -> Self {
106 Self {
107 position: Default::default(),
108 items: Default::default(),
109 selected_index: Default::default(),
110 visible: Default::default(),
111 previously_focused_view_id: Default::default(),
112 _actions_observation: cx.observe_actions(Self::action_dispatched),
113 }
114 }
115
116 fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
117 if let Some(ix) = self
118 .items
119 .iter()
120 .position(|item| item.action_id() == Some(action_id))
121 {
122 self.selected_index = Some(ix);
123 cx.notify();
124 cx.spawn(|this, mut cx| async move {
125 cx.background().timer(Duration::from_millis(100)).await;
126 this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
127 })
128 .detach();
129 }
130 }
131
132 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
133 if let Some(ix) = self.selected_index {
134 if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
135 let window_id = cx.window_id();
136 let view_id = cx.view_id();
137 cx.dispatch_action_at(window_id, view_id, action.as_ref());
138 }
139 }
140 }
141
142 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
143 self.reset(cx);
144 cx.defer(|this, cx| {
145 if cx.handle().is_focused(cx) {
146 let window_id = cx.window_id();
147 (**cx).focus(window_id, this.previously_focused_view_id.take());
148 }
149 });
150 }
151
152 fn reset(&mut self, cx: &mut ViewContext<Self>) {
153 self.items.clear();
154 self.visible = false;
155 self.selected_index.take();
156 cx.notify();
157 }
158
159 fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
160 self.selected_index = self.items.iter().position(|item| !item.is_separator());
161 cx.notify();
162 }
163
164 fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
165 for (ix, item) in self.items.iter().enumerate().rev() {
166 if !item.is_separator() {
167 self.selected_index = Some(ix);
168 cx.notify();
169 break;
170 }
171 }
172 }
173
174 fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
175 if let Some(ix) = self.selected_index {
176 for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
177 if !item.is_separator() {
178 self.selected_index = Some(ix);
179 cx.notify();
180 break;
181 }
182 }
183 } else {
184 self.select_first(&Default::default(), cx);
185 }
186 }
187
188 fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
189 if let Some(ix) = self.selected_index {
190 for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
191 if !item.is_separator() {
192 self.selected_index = Some(ix);
193 cx.notify();
194 break;
195 }
196 }
197 } else {
198 self.select_last(&Default::default(), cx);
199 }
200 }
201
202 pub fn show(
203 &mut self,
204 position: Vector2F,
205 items: impl IntoIterator<Item = ContextMenuItem>,
206 cx: &mut ViewContext<Self>,
207 ) {
208 let mut items = items.into_iter().peekable();
209 if items.peek().is_some() {
210 self.items = items.collect();
211 self.position = position;
212 self.visible = true;
213 if !cx.is_self_focused() {
214 self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
215 }
216 cx.focus_self();
217 } else {
218 self.visible = false;
219 }
220 cx.notify();
221 }
222
223 fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
224 let style = cx.global::<Settings>().theme.context_menu.clone();
225 Flex::row()
226 .with_child(
227 Flex::column()
228 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
229 match item {
230 ContextMenuItem::Item { label, .. } => {
231 let style = style
232 .item
233 .style_for(Default::default(), Some(ix) == self.selected_index);
234 Label::new(label.to_string(), style.label.clone())
235 .contained()
236 .with_style(style.container)
237 .boxed()
238 }
239 ContextMenuItem::Separator => Empty::new()
240 .collapsed()
241 .contained()
242 .with_style(style.separator)
243 .constrained()
244 .with_height(1.)
245 .boxed(),
246 }
247 }))
248 .boxed(),
249 )
250 .with_child(
251 Flex::column()
252 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
253 match item {
254 ContextMenuItem::Item { action, .. } => {
255 let style = style
256 .item
257 .style_for(Default::default(), Some(ix) == self.selected_index);
258 KeystrokeLabel::new(
259 action.boxed_clone(),
260 style.keystroke.container,
261 style.keystroke.text.clone(),
262 )
263 .boxed()
264 }
265 ContextMenuItem::Separator => Empty::new()
266 .collapsed()
267 .constrained()
268 .with_height(1.)
269 .contained()
270 .with_style(style.separator)
271 .boxed(),
272 }
273 }))
274 .boxed(),
275 )
276 .contained()
277 .with_style(style.container)
278 }
279
280 fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
281 enum Menu {}
282 enum MenuItem {}
283 let style = cx.global::<Settings>().theme.context_menu.clone();
284 MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
285 Flex::column()
286 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
287 match item {
288 ContextMenuItem::Item { label, action } => {
289 let action = action.boxed_clone();
290 MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
291 let style =
292 style.item.style_for(state, Some(ix) == self.selected_index);
293 Flex::row()
294 .with_child(
295 Label::new(label.to_string(), style.label.clone()).boxed(),
296 )
297 .with_child({
298 KeystrokeLabel::new(
299 action.boxed_clone(),
300 style.keystroke.container,
301 style.keystroke.text.clone(),
302 )
303 .flex_float()
304 .boxed()
305 })
306 .contained()
307 .with_style(style.container)
308 .boxed()
309 })
310 .with_cursor_style(CursorStyle::PointingHand)
311 .on_click(move |_, _, cx| {
312 cx.dispatch_any_action(action.boxed_clone());
313 cx.dispatch_action(Cancel);
314 })
315 .boxed()
316 }
317 ContextMenuItem::Separator => Empty::new()
318 .constrained()
319 .with_height(1.)
320 .contained()
321 .with_style(style.separator)
322 .boxed(),
323 }
324 }))
325 .contained()
326 .with_style(style.container)
327 .boxed()
328 })
329 .on_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
330 .on_right_mouse_down_out(|_, cx| cx.dispatch_action(Cancel))
331 }
332}