gpui_component/menu/
app_menu_bar.rs

1use crate::{
2    Selectable, Sizable,
3    actions::{Cancel, SelectLeft, SelectRight},
4    button::{Button, ButtonVariants},
5    h_flex,
6    menu::PopupMenu,
7};
8use gpui::{
9    App, AppContext as _, ClickEvent, Context, DismissEvent, Entity, Focusable,
10    InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu, ParentElement,
11    Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, anchored,
12    deferred, div, prelude::FluentBuilder, px,
13};
14
15const CONTEXT: &str = "AppMenuBar";
16pub fn init(cx: &mut App) {
17    cx.bind_keys([
18        KeyBinding::new("escape", Cancel, Some(CONTEXT)),
19        KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
20        KeyBinding::new("right", SelectRight, Some(CONTEXT)),
21    ]);
22}
23
24/// The application menu bar, for Windows and Linux.
25pub struct AppMenuBar {
26    menus: Vec<Entity<AppMenu>>,
27    selected_ix: Option<usize>,
28}
29
30impl AppMenuBar {
31    /// Create a new app menu bar.
32    pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
33        cx.new(|cx| {
34            let menu_bar = cx.entity();
35            let menus = cx
36                .get_menus()
37                .unwrap_or_default()
38                .iter()
39                .enumerate()
40                .map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), window, cx))
41                .collect();
42
43            Self {
44                selected_ix: None,
45                menus,
46            }
47        })
48    }
49
50    fn on_move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
51        let Some(selected_ix) = self.selected_ix else {
52            return;
53        };
54
55        let new_ix = if selected_ix == 0 {
56            self.menus.len().saturating_sub(1)
57        } else {
58            selected_ix.saturating_sub(1)
59        };
60        self.set_selected_index(Some(new_ix), window, cx);
61    }
62
63    fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
64        let Some(selected_ix) = self.selected_ix else {
65            return;
66        };
67
68        let new_ix = if selected_ix + 1 >= self.menus.len() {
69            0
70        } else {
71            selected_ix + 1
72        };
73        self.set_selected_index(Some(new_ix), window, cx);
74    }
75
76    fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
77        self.set_selected_index(None, window, cx);
78    }
79
80    fn set_selected_index(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
81        self.selected_ix = ix;
82        cx.notify();
83    }
84
85    #[inline]
86    fn has_activated_menu(&self) -> bool {
87        self.selected_ix.is_some()
88    }
89}
90
91impl Render for AppMenuBar {
92    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
93        h_flex()
94            .id("app-menu-bar")
95            .key_context(CONTEXT)
96            .on_action(cx.listener(Self::on_move_left))
97            .on_action(cx.listener(Self::on_move_right))
98            .on_action(cx.listener(Self::on_cancel))
99            .size_full()
100            .gap_x_1()
101            .overflow_x_scroll()
102            .children(self.menus.clone())
103    }
104}
105
106/// A menu in the menu bar.
107pub(super) struct AppMenu {
108    menu_bar: Entity<AppMenuBar>,
109    ix: usize,
110    name: SharedString,
111    menu: OwnedMenu,
112    popup_menu: Option<Entity<PopupMenu>>,
113
114    _subscription: Option<Subscription>,
115}
116
117impl AppMenu {
118    pub(super) fn new(
119        ix: usize,
120        menu: &OwnedMenu,
121        menu_bar: Entity<AppMenuBar>,
122        _: &mut Window,
123        cx: &mut App,
124    ) -> Entity<Self> {
125        let name = menu.name.clone();
126        cx.new(|_| Self {
127            ix,
128            menu_bar,
129            name,
130            menu: menu.clone(),
131            popup_menu: None,
132            _subscription: None,
133        })
134    }
135
136    fn build_popup_menu(
137        &mut self,
138        window: &mut Window,
139        cx: &mut Context<Self>,
140    ) -> Entity<PopupMenu> {
141        let popup_menu = match self.popup_menu.as_ref() {
142            None => {
143                let items = self.menu.items.clone();
144                let popup_menu = PopupMenu::build(window, cx, |menu, window, cx| {
145                    menu.when_some(window.focused(cx), |this, handle| {
146                        this.action_context(handle)
147                    })
148                    .with_menu_items(items, window, cx)
149                });
150                popup_menu.read(cx).focus_handle(cx).focus(window);
151                self._subscription =
152                    Some(cx.subscribe_in(&popup_menu, window, Self::handle_dismiss));
153                self.popup_menu = Some(popup_menu.clone());
154
155                popup_menu
156            }
157            Some(menu) => menu.clone(),
158        };
159
160        let focus_handle = popup_menu.read(cx).focus_handle(cx);
161        if !focus_handle.contains_focused(window, cx) {
162            focus_handle.focus(window);
163        }
164
165        popup_menu
166    }
167
168    fn handle_dismiss(
169        &mut self,
170        _: &Entity<PopupMenu>,
171        _: &DismissEvent,
172        window: &mut Window,
173        cx: &mut Context<Self>,
174    ) {
175        self._subscription.take();
176        self.popup_menu.take();
177        self.menu_bar.update(cx, |state, cx| {
178            state.on_cancel(&Cancel, window, cx);
179        });
180    }
181
182    fn handle_trigger_click(
183        &mut self,
184        _: &ClickEvent,
185        window: &mut Window,
186        cx: &mut Context<Self>,
187    ) {
188        let is_selected = self.menu_bar.read(cx).selected_ix == Some(self.ix);
189
190        _ = self.menu_bar.update(cx, |state, cx| {
191            let new_ix = if is_selected { None } else { Some(self.ix) };
192            state.set_selected_index(new_ix, window, cx);
193        });
194    }
195
196    fn handle_hover(&mut self, hovered: &bool, window: &mut Window, cx: &mut Context<Self>) {
197        if !*hovered {
198            return;
199        }
200
201        let has_activated_menu = self.menu_bar.read(cx).has_activated_menu();
202        if !has_activated_menu {
203            return;
204        }
205
206        _ = self.menu_bar.update(cx, |state, cx| {
207            state.set_selected_index(Some(self.ix), window, cx);
208        });
209    }
210}
211
212impl Render for AppMenu {
213    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
214        let menu_bar = self.menu_bar.read(cx);
215        let is_selected = menu_bar.selected_ix == Some(self.ix);
216
217        div()
218            .id(self.ix)
219            .relative()
220            .child(
221                Button::new("menu")
222                    .small()
223                    .py_0p5()
224                    .compact()
225                    .ghost()
226                    .label(self.name.clone())
227                    .selected(is_selected)
228                    .on_mouse_down(MouseButton::Left, |_, window, cx| {
229                        // Stop propagation to avoid dragging the window.
230                        window.prevent_default();
231                        cx.stop_propagation();
232                    })
233                    .on_click(cx.listener(Self::handle_trigger_click)),
234            )
235            .on_hover(cx.listener(Self::handle_hover))
236            .when(is_selected, |this| {
237                this.child(deferred(
238                    anchored()
239                        .anchor(gpui::Corner::TopLeft)
240                        .snap_to_window_with_margin(px(8.))
241                        .child(
242                            div()
243                                .size_full()
244                                .occlude()
245                                .top_1()
246                                .child(self.build_popup_menu(window, cx)),
247                        ),
248                ))
249            })
250    }
251}