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