gpui_ui_kit/
menu.rs

1//! Menu components - MenuItem, Menu, MenuBar, and ContextMenu
2//!
3//! Provides a complete menu system for application navigation and context menus.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// A single menu item
9#[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    /// Create a new menu item
24    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    /// Create a separator item
39    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    /// Create a checkbox menu item
54    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    /// Add a keyboard shortcut display
73    pub fn with_shortcut(mut self, shortcut: impl Into<SharedString>) -> Self {
74        self.shortcut = Some(shortcut.into());
75        self
76    }
77
78    /// Add an icon
79    pub fn with_icon(mut self, icon: impl Into<SharedString>) -> Self {
80        self.icon = Some(icon.into());
81        self
82    }
83
84    /// Disable the menu item
85    pub fn disabled(mut self, disabled: bool) -> Self {
86        self.disabled = disabled;
87        self
88    }
89
90    /// Add submenu items
91    pub fn with_children(mut self, children: Vec<MenuItem>) -> Self {
92        self.children = children;
93        self
94    }
95
96    /// Get the item ID
97    pub fn id(&self) -> &SharedString {
98        &self.id
99    }
100
101    /// Check if this is a separator
102    pub fn is_separator(&self) -> bool {
103        self.is_separator
104    }
105}
106
107/// A dropdown menu containing menu items
108pub 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    /// Create a new menu with items
116    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    /// Set minimum width
125    pub fn min_width(mut self, width: Pixels) -> Self {
126        self.min_width = width;
127        self
128    }
129
130    /// Set the selection handler
131    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    /// Build into element
140    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                // Checkbox indicator
199                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                // Icon
208                if let Some(icon) = icon {
209                    row = row.child(div().w(px(16.0)).child(icon));
210                }
211
212                // Label (flex-1 to push shortcut to right)
213                row = row.child(div().flex_1().child(label));
214
215                // Shortcut
216                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
236/// A menu bar item (top-level menu)
237pub struct MenuBarItem {
238    id: SharedString,
239    label: SharedString,
240    items: Vec<MenuItem>,
241}
242
243impl MenuBarItem {
244    /// Create a new menu bar item
245    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    /// Set the dropdown items
254    pub fn with_items(mut self, items: Vec<MenuItem>) -> Self {
255        self.items = items;
256        self
257    }
258
259    /// Get the menu ID
260    pub fn id(&self) -> &SharedString {
261        &self.id
262    }
263
264    /// Get the menu label
265    pub fn label(&self) -> &SharedString {
266        &self.label
267    }
268
269    /// Get the menu items
270    pub fn items(&self) -> &[MenuItem] {
271        &self.items
272    }
273}
274
275/// A horizontal menu bar
276pub 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    /// Create a new menu bar
285    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    /// Set the currently active (open) menu
295    pub fn active_menu(mut self, id: Option<SharedString>) -> Self {
296        self.active_menu = id;
297        self
298    }
299
300    /// Set the item selection handler
301    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    /// Set the menu toggle handler
310    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    /// Get menu bar items (for external rendering with custom handlers)
319    pub fn items(&self) -> &[MenuBarItem] {
320        &self.items
321    }
322
323    /// Get active menu ID
324    pub fn get_active_menu(&self) -> Option<&SharedString> {
325        self.active_menu.as_ref()
326    }
327
328    /// Build into element
329    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
386/// Helper to build a single menu bar button without handlers
387/// Use this when you need to add cx.listener() handlers
388pub 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}