gpui_component/menu/
app_menu_bar.rs

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