1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Clone)]
10pub struct MenuItem {
11 id: SharedString,
12 label: SharedString,
13 shortcut: Option<SharedString>,
14 icon: Option<SharedString>,
15 disabled: bool,
16 is_separator: bool,
17 is_checkbox: bool,
18 checked: bool,
19 children: Vec<MenuItem>,
20}
21
22impl MenuItem {
23 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
25 Self {
26 id: id.into(),
27 label: label.into(),
28 shortcut: None,
29 icon: None,
30 disabled: false,
31 is_separator: false,
32 is_checkbox: false,
33 checked: false,
34 children: Vec::new(),
35 }
36 }
37
38 pub fn separator() -> Self {
40 Self {
41 id: "separator".into(),
42 label: "".into(),
43 shortcut: None,
44 icon: None,
45 disabled: true,
46 is_separator: true,
47 is_checkbox: false,
48 checked: false,
49 children: Vec::new(),
50 }
51 }
52
53 pub fn checkbox(
55 id: impl Into<SharedString>,
56 label: impl Into<SharedString>,
57 checked: bool,
58 ) -> Self {
59 Self {
60 id: id.into(),
61 label: label.into(),
62 shortcut: None,
63 icon: None,
64 disabled: false,
65 is_separator: false,
66 is_checkbox: true,
67 checked,
68 children: Vec::new(),
69 }
70 }
71
72 pub fn with_shortcut(mut self, shortcut: impl Into<SharedString>) -> Self {
74 self.shortcut = Some(shortcut.into());
75 self
76 }
77
78 pub fn with_icon(mut self, icon: impl Into<SharedString>) -> Self {
80 self.icon = Some(icon.into());
81 self
82 }
83
84 pub fn disabled(mut self, disabled: bool) -> Self {
86 self.disabled = disabled;
87 self
88 }
89
90 pub fn with_children(mut self, children: Vec<MenuItem>) -> Self {
92 self.children = children;
93 self
94 }
95
96 pub fn id(&self) -> &SharedString {
98 &self.id
99 }
100
101 pub fn is_separator(&self) -> bool {
103 self.is_separator
104 }
105}
106
107pub struct Menu {
109 items: Vec<MenuItem>,
110 min_width: Pixels,
111 on_select: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
112}
113
114impl Menu {
115 pub fn new(items: Vec<MenuItem>) -> Self {
117 Self {
118 items,
119 min_width: px(180.0),
120 on_select: None,
121 }
122 }
123
124 pub fn min_width(mut self, width: Pixels) -> Self {
126 self.min_width = width;
127 self
128 }
129
130 pub fn on_select(
132 mut self,
133 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
134 ) -> Self {
135 self.on_select = Some(Box::new(handler));
136 self
137 }
138
139 pub fn build(self) -> Stateful<Div> {
141 let min_width = self.min_width;
142
143 let mut menu = div()
144 .id("menu-container")
145 .min_w(min_width)
146 .max_h(px(400.0))
147 .bg(rgb(0x2a2a2a))
148 .border_1()
149 .border_color(rgb(0x444444))
150 .rounded(px(4.0))
151 .shadow_lg()
152 .py_1()
153 .overflow_y_scroll();
154
155 for item in self.items {
156 if item.is_separator {
157 menu = menu.child(div().my_1().h(px(1.0)).bg(rgb(0x3a3a3a)).mx_2());
158 } else {
159 let item_id = item.id.clone();
160 let label = item.label.clone();
161 let shortcut = item.shortcut.clone();
162 let icon = item.icon.clone();
163 let disabled = item.disabled;
164 let is_checkbox = item.is_checkbox;
165 let checked = item.checked;
166
167 let on_select: Option<*const dyn Fn(&SharedString, &mut Window, &mut App)> =
168 self.on_select.as_ref().map(|f| f.as_ref() as *const _);
169
170 let mut row = div()
171 .id(SharedString::from(format!("menu-item-{}", item_id)))
172 .px_3()
173 .py(px(6.0))
174 .mx_1()
175 .rounded(px(3.0))
176 .flex()
177 .items_center()
178 .gap_2()
179 .text_sm();
180
181 if disabled {
182 row = row.text_color(rgb(0x666666)).cursor_not_allowed();
183 } else {
184 row = row
185 .text_color(rgb(0xcccccc))
186 .cursor_pointer()
187 .hover(|s| s.bg(rgb(0x3a3a3a)).text_color(rgb(0xffffff)));
188
189 if let Some(handler_ptr) = on_select {
190 let id = item_id.clone();
191 row =
192 row.on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
193 (*handler_ptr)(&id, window, cx);
194 });
195 }
196 }
197
198 if is_checkbox {
200 row = row.child(div().w(px(16.0)).text_xs().child(if checked {
201 "✓"
202 } else {
203 " "
204 }));
205 }
206
207 if let Some(icon) = icon {
209 row = row.child(div().w(px(16.0)).child(icon));
210 }
211
212 row = row.child(div().flex_1().child(label));
214
215 if let Some(shortcut) = shortcut {
217 row = row.child(div().text_xs().text_color(rgb(0x777777)).child(shortcut));
218 }
219
220 menu = menu.child(row);
221 }
222 }
223
224 menu
225 }
226}
227
228impl IntoElement for Menu {
229 type Element = Stateful<Div>;
230
231 fn into_element(self) -> Self::Element {
232 self.build()
233 }
234}
235
236pub struct MenuBarItem {
238 id: SharedString,
239 label: SharedString,
240 items: Vec<MenuItem>,
241}
242
243impl MenuBarItem {
244 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
246 Self {
247 id: id.into(),
248 label: label.into(),
249 items: Vec::new(),
250 }
251 }
252
253 pub fn with_items(mut self, items: Vec<MenuItem>) -> Self {
255 self.items = items;
256 self
257 }
258
259 pub fn id(&self) -> &SharedString {
261 &self.id
262 }
263
264 pub fn label(&self) -> &SharedString {
266 &self.label
267 }
268
269 pub fn items(&self) -> &[MenuItem] {
271 &self.items
272 }
273}
274
275pub struct MenuBar {
277 items: Vec<MenuBarItem>,
278 active_menu: Option<SharedString>,
279 on_select: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
280 on_menu_toggle: Option<Box<dyn Fn(Option<&SharedString>, &mut Window, &mut App) + 'static>>,
281}
282
283impl MenuBar {
284 pub fn new(items: Vec<MenuBarItem>) -> Self {
286 Self {
287 items,
288 active_menu: None,
289 on_select: None,
290 on_menu_toggle: None,
291 }
292 }
293
294 pub fn active_menu(mut self, id: Option<SharedString>) -> Self {
296 self.active_menu = id;
297 self
298 }
299
300 pub fn on_select(
302 mut self,
303 handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
304 ) -> Self {
305 self.on_select = Some(Box::new(handler));
306 self
307 }
308
309 pub fn on_menu_toggle(
311 mut self,
312 handler: impl Fn(Option<&SharedString>, &mut Window, &mut App) + 'static,
313 ) -> Self {
314 self.on_menu_toggle = Some(Box::new(handler));
315 self
316 }
317
318 pub fn items(&self) -> &[MenuBarItem] {
320 &self.items
321 }
322
323 pub fn get_active_menu(&self) -> Option<&SharedString> {
325 self.active_menu.as_ref()
326 }
327
328 pub fn build(self) -> Div {
330 let mut bar = div().flex().items_center().gap_1();
331
332 for item in &self.items {
333 let is_open = self.active_menu.as_ref() == Some(&item.id);
334 let menu_id = item.id.clone();
335 let label = item.label.clone();
336 let on_toggle: Option<*const dyn Fn(Option<&SharedString>, &mut Window, &mut App)> =
337 self.on_menu_toggle.as_ref().map(|f| f.as_ref() as *const _);
338
339 let mut button = div()
340 .id(SharedString::from(format!("menubar-{}", menu_id)))
341 .px_3()
342 .py_1()
343 .rounded(px(3.0))
344 .text_sm()
345 .cursor_pointer();
346
347 if is_open {
348 button = button
349 .bg(rgb(0x3a3a3a))
350 .font_weight(FontWeight::BOLD)
351 .text_color(rgb(0xffffff));
352 } else {
353 button = button
354 .text_color(rgb(0xcccccc))
355 .hover(|s| s.bg(rgb(0x333333)));
356 }
357
358 if let Some(handler_ptr) = on_toggle {
359 let id = menu_id.clone();
360 let currently_open = is_open;
361 button = button.on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
362 if currently_open {
363 (*handler_ptr)(None, window, cx);
364 } else {
365 (*handler_ptr)(Some(&id), window, cx);
366 }
367 });
368 }
369
370 button = button.child(label);
371 bar = bar.child(button);
372 }
373
374 bar
375 }
376}
377
378impl IntoElement for MenuBar {
379 type Element = Div;
380
381 fn into_element(self) -> Self::Element {
382 self.build()
383 }
384}
385
386pub fn menu_bar_button(
389 id: impl Into<SharedString>,
390 label: impl Into<SharedString>,
391 is_open: bool,
392) -> Stateful<Div> {
393 let id = id.into();
394 let label = label.into();
395
396 let mut button = div()
397 .id(SharedString::from(format!("menubar-{}", id)))
398 .px_3()
399 .py_1()
400 .rounded(px(3.0))
401 .text_sm()
402 .cursor_pointer();
403
404 if is_open {
405 button = button
406 .bg(rgb(0x3a3a3a))
407 .font_weight(FontWeight::BOLD)
408 .text_color(rgb(0xffffff));
409 } else {
410 button = button
411 .text_color(rgb(0xcccccc))
412 .hover(|s| s.bg(rgb(0x333333)));
413 }
414
415 button.child(label)
416}