1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone)]
10pub struct MenuTheme {
11 pub background: Rgba,
13 pub border: Rgba,
15 pub separator: Rgba,
17 pub text: Rgba,
19 pub text_hover: Rgba,
21 pub text_disabled: Rgba,
23 pub text_shortcut: Rgba,
25 pub hover_bg: Rgba,
27 pub danger_hover_bg: Rgba,
29}
30
31impl Default for MenuTheme {
32 fn default() -> Self {
33 Self {
34 background: rgba(0x2a2a2aff),
35 border: rgba(0x444444ff),
36 separator: rgba(0x3a3a3aff),
37 text: rgba(0xccccccff),
38 text_hover: rgba(0xffffffff),
39 text_disabled: rgba(0x666666ff),
40 text_shortcut: rgba(0x777777ff),
41 hover_bg: rgba(0x3a3a3aff),
42 danger_hover_bg: rgba(0xdc2626ff),
43 }
44 }
45}
46
47#[derive(Clone)]
49pub struct MenuItem {
50 id: SharedString,
51 label: SharedString,
52 shortcut: Option<SharedString>,
53 icon: Option<SharedString>,
54 disabled: bool,
55 is_separator: bool,
56 is_checkbox: bool,
57 checked: bool,
58 is_danger: bool,
59 children: Vec<MenuItem>,
60}
61
62impl MenuItem {
63 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
65 Self {
66 id: id.into(),
67 label: label.into(),
68 shortcut: None,
69 icon: None,
70 disabled: false,
71 is_separator: false,
72 is_checkbox: false,
73 checked: false,
74 is_danger: false,
75 children: Vec::new(),
76 }
77 }
78
79 pub fn separator() -> Self {
81 Self {
82 id: "separator".into(),
83 label: "".into(),
84 shortcut: None,
85 icon: None,
86 disabled: true,
87 is_separator: true,
88 is_checkbox: false,
89 checked: false,
90 is_danger: false,
91 children: Vec::new(),
92 }
93 }
94
95 pub fn checkbox(
97 id: impl Into<SharedString>,
98 label: impl Into<SharedString>,
99 checked: bool,
100 ) -> Self {
101 Self {
102 id: id.into(),
103 label: label.into(),
104 shortcut: None,
105 icon: None,
106 disabled: false,
107 is_separator: false,
108 is_checkbox: true,
109 checked,
110 is_danger: false,
111 children: Vec::new(),
112 }
113 }
114
115 pub fn with_shortcut(mut self, shortcut: impl Into<SharedString>) -> Self {
117 self.shortcut = Some(shortcut.into());
118 self
119 }
120
121 pub fn with_icon(mut self, icon: impl Into<SharedString>) -> Self {
123 self.icon = Some(icon.into());
124 self
125 }
126
127 pub fn disabled(mut self, disabled: bool) -> Self {
129 self.disabled = disabled;
130 self
131 }
132
133 pub fn with_children(mut self, children: Vec<MenuItem>) -> Self {
135 self.children = children;
136 self
137 }
138
139 pub fn id(&self) -> &SharedString {
141 &self.id
142 }
143
144 pub fn is_separator(&self) -> bool {
146 self.is_separator
147 }
148
149 pub fn danger(mut self) -> Self {
151 self.is_danger = true;
152 self
153 }
154
155 pub fn is_danger(&self) -> bool {
157 self.is_danger
158 }
159}
160
161pub struct Menu {
163 items: Vec<MenuItem>,
164 min_width: Pixels,
165 theme: Option<MenuTheme>,
166 on_select: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
167}
168
169impl Menu {
170 pub fn new(items: Vec<MenuItem>) -> Self {
172 Self {
173 items,
174 min_width: px(180.0),
175 theme: None,
176 on_select: None,
177 }
178 }
179
180 pub fn min_width(mut self, width: Pixels) -> Self {
182 self.min_width = width;
183 self
184 }
185
186 pub fn theme(mut self, theme: MenuTheme) -> Self {
188 self.theme = Some(theme);
189 self
190 }
191
192 pub fn on_select(
194 mut self,
195 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
196 ) -> Self {
197 self.on_select = Some(Box::new(handler));
198 self
199 }
200
201 pub fn build(self) -> Stateful<Div> {
203 let min_width = self.min_width;
204 let default_theme = MenuTheme::default();
205 let theme = self.theme.as_ref().unwrap_or(&default_theme);
206
207 let mut menu = div()
208 .id("menu-container")
209 .min_w(min_width)
210 .max_h(px(400.0))
211 .bg(theme.background)
212 .border_1()
213 .border_color(theme.border)
214 .rounded(px(4.0))
215 .shadow_lg()
216 .py_1()
217 .overflow_y_scroll();
218
219 for item in self.items {
220 if item.is_separator {
221 menu = menu.child(div().my_1().h(px(1.0)).bg(theme.separator).mx_2());
222 } else {
223 let item_id = item.id.clone();
224 let label = item.label.clone();
225 let shortcut = item.shortcut.clone();
226 let icon = item.icon.clone();
227 let disabled = item.disabled;
228 let is_checkbox = item.is_checkbox;
229 let checked = item.checked;
230 let is_danger = item.is_danger;
231
232 let on_select: Option<*const dyn Fn(&SharedString, &mut Window, &mut App)> =
233 self.on_select.as_ref().map(|f| f.as_ref() as *const _);
234
235 let mut row = div()
236 .id(SharedString::from(format!("menu-item-{}", item_id)))
237 .px_3()
238 .py(px(6.0))
239 .mx_1()
240 .rounded(px(3.0))
241 .flex()
242 .items_center()
243 .gap_2()
244 .text_sm();
245
246 if disabled {
247 row = row.text_color(theme.text_disabled).cursor_not_allowed();
248 } else {
249 let text_color = theme.text;
250 let text_hover = theme.text_hover;
251 let hover_bg = if is_danger {
252 theme.danger_hover_bg
253 } else {
254 theme.hover_bg
255 };
256
257 row = row
258 .text_color(text_color)
259 .cursor_pointer()
260 .hover(move |s| s.bg(hover_bg).text_color(text_hover));
261
262 if let Some(handler_ptr) = on_select {
263 let id = item_id.clone();
264 row =
265 row.on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
266 (*handler_ptr)(&id, window, cx);
267 });
268 }
269 }
270
271 if is_checkbox {
273 row = row.child(div().w(px(16.0)).text_xs().child(if checked {
274 "✓"
275 } else {
276 " "
277 }));
278 }
279
280 if let Some(icon) = icon {
282 row = row.child(div().w(px(16.0)).child(icon));
283 }
284
285 row = row.child(div().flex_1().child(label));
287
288 if let Some(shortcut) = shortcut {
290 let shortcut_color = theme.text_shortcut;
291 row = row.child(div().text_xs().text_color(shortcut_color).child(shortcut));
292 }
293
294 menu = menu.child(row);
295 }
296 }
297
298 menu
299 }
300}
301
302impl IntoElement for Menu {
303 type Element = Stateful<Div>;
304
305 fn into_element(self) -> Self::Element {
306 self.build()
307 }
308}
309
310pub struct MenuBarItem {
312 id: SharedString,
313 label: SharedString,
314 items: Vec<MenuItem>,
315}
316
317impl MenuBarItem {
318 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
320 Self {
321 id: id.into(),
322 label: label.into(),
323 items: Vec::new(),
324 }
325 }
326
327 pub fn with_items(mut self, items: Vec<MenuItem>) -> Self {
329 self.items = items;
330 self
331 }
332
333 pub fn id(&self) -> &SharedString {
335 &self.id
336 }
337
338 pub fn label(&self) -> &SharedString {
340 &self.label
341 }
342
343 pub fn items(&self) -> &[MenuItem] {
345 &self.items
346 }
347}
348
349pub struct MenuBar {
351 items: Vec<MenuBarItem>,
352 active_menu: Option<SharedString>,
353 on_select: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
354 on_menu_toggle: Option<Box<dyn Fn(Option<&SharedString>, &mut Window, &mut App) + 'static>>,
355}
356
357impl MenuBar {
358 pub fn new(items: Vec<MenuBarItem>) -> Self {
360 Self {
361 items,
362 active_menu: None,
363 on_select: None,
364 on_menu_toggle: None,
365 }
366 }
367
368 pub fn active_menu(mut self, id: Option<SharedString>) -> Self {
370 self.active_menu = id;
371 self
372 }
373
374 pub fn on_select(
376 mut self,
377 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
378 ) -> Self {
379 self.on_select = Some(Box::new(handler));
380 self
381 }
382
383 pub fn on_menu_toggle(
385 mut self,
386 handler: impl Fn(Option<&SharedString>, &mut Window, &mut App) + 'static,
387 ) -> Self {
388 self.on_menu_toggle = Some(Box::new(handler));
389 self
390 }
391
392 pub fn items(&self) -> &[MenuBarItem] {
394 &self.items
395 }
396
397 pub fn get_active_menu(&self) -> Option<&SharedString> {
399 self.active_menu.as_ref()
400 }
401
402 pub fn build(self) -> Div {
404 let mut bar = div().flex().items_center().gap_1();
405
406 for item in &self.items {
407 let is_open = self.active_menu.as_ref() == Some(&item.id);
408 let menu_id = item.id.clone();
409 let label = item.label.clone();
410 let on_toggle: Option<*const dyn Fn(Option<&SharedString>, &mut Window, &mut App)> =
411 self.on_menu_toggle.as_ref().map(|f| f.as_ref() as *const _);
412
413 let mut button = div()
414 .id(SharedString::from(format!("menubar-{}", menu_id)))
415 .px_3()
416 .py_1()
417 .rounded(px(3.0))
418 .text_sm()
419 .cursor_pointer();
420
421 if is_open {
422 button = button
423 .bg(rgb(0x3a3a3a))
424 .font_weight(FontWeight::BOLD)
425 .text_color(rgb(0xffffff));
426 } else {
427 button = button
428 .text_color(rgb(0xcccccc))
429 .hover(|s| s.bg(rgb(0x333333)));
430 }
431
432 if let Some(handler_ptr) = on_toggle {
433 let id = menu_id.clone();
434 let currently_open = is_open;
435 button = button.on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
436 if currently_open {
437 (*handler_ptr)(None, window, cx);
438 } else {
439 (*handler_ptr)(Some(&id), window, cx);
440 }
441 });
442 }
443
444 button = button.child(label);
445 bar = bar.child(button);
446 }
447
448 bar
449 }
450}
451
452impl IntoElement for MenuBar {
453 type Element = Div;
454
455 fn into_element(self) -> Self::Element {
456 self.build()
457 }
458}
459
460pub fn menu_bar_button(
463 id: impl Into<SharedString>,
464 label: impl Into<SharedString>,
465 is_open: bool,
466) -> Stateful<Div> {
467 let id = id.into();
468 let label = label.into();
469
470 let mut button = div()
471 .id(SharedString::from(format!("menubar-{}", id)))
472 .px_3()
473 .py_1()
474 .rounded(px(3.0))
475 .text_sm()
476 .cursor_pointer();
477
478 if is_open {
479 button = button
480 .bg(rgb(0x3a3a3a))
481 .font_weight(FontWeight::BOLD)
482 .text_color(rgb(0xffffff));
483 } else {
484 button = button
485 .text_color(rgb(0xcccccc))
486 .hover(|s| s.bg(rgb(0x333333)));
487 }
488
489 button.child(label)
490}