1use gpui::{
38 App, Context, EventEmitter, FocusHandle, Focusable, KeyBinding, SharedString, Window, actions,
39 div, prelude::*, px,
40};
41
42use theme::{TextStyle, Theme, Typeset};
43
44use crate::{
45 menu::{self, Item},
46 popover,
47};
48
49#[derive(Clone, Debug)]
51pub struct Menu {
52 pub title: SharedString,
53 pub items: Vec<Item>,
54}
55
56impl Menu {
57 pub fn new(title: impl Into<SharedString>, items: Vec<Item>) -> Self {
58 Self {
59 title: title.into(),
60 items,
61 }
62 }
63}
64
65actions!(
70 bezel_menubar,
71 [PrevMenu, NextMenu, PrevItem, NextItem, Confirm, Dismiss]
72);
73
74pub const KEY_CONTEXT: &str = "Menubar";
77
78pub fn init(cx: &mut App) {
85 let ctx = Some(KEY_CONTEXT);
86 cx.bind_keys([
87 KeyBinding::new("left", PrevMenu, ctx),
88 KeyBinding::new("right", NextMenu, ctx),
89 KeyBinding::new("up", PrevItem, ctx),
90 KeyBinding::new("down", NextItem, ctx),
91 KeyBinding::new("enter", Confirm, ctx),
92 KeyBinding::new("escape", Dismiss, ctx),
93 ]);
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub enum MenubarEvent {
99 Selected { menu: usize, item: usize },
100}
101
102pub struct Menubar {
103 menus: Vec<Menu>,
104 open: popover::Popup<usize>,
108 highlighted: Option<usize>,
112 focus_handle: FocusHandle,
113}
114
115impl EventEmitter<MenubarEvent> for Menubar {}
116
117impl Menubar {
118 pub fn new(menus: Vec<Menu>, cx: &mut Context<Self>) -> Self {
119 Self {
120 menus,
121 open: popover::Popup::default(),
122 highlighted: None,
123 focus_handle: cx.focus_handle().tab_stop(true),
126 }
127 }
128
129 pub fn open_menu(&self) -> Option<usize> {
131 self.open.as_open().copied()
132 }
133
134 pub fn menus(&self) -> &[Menu] {
138 &self.menus
139 }
140
141 fn show(&mut self, menu: usize, window: &mut Window, cx: &mut Context<Self>) {
142 self.open.open(menu);
143 self.highlighted = None;
144 window.focus(&self.focus_handle, cx);
145 cx.notify();
146 }
147
148 fn toggle(&mut self, menu: usize, window: &mut Window, cx: &mut Context<Self>) {
149 if self.open.take_press_was_open() {
153 self.close(cx);
154 } else {
155 self.show(menu, window, cx);
156 }
157 }
158
159 fn hover_switch(&mut self, menu: usize, cx: &mut Context<Self>) {
164 if self.open.is_open() && self.open_menu() != Some(menu) {
165 self.open.open(menu);
166 self.highlighted = None;
167 cx.notify();
168 }
169 }
170
171 fn close(&mut self, cx: &mut Context<Self>) {
172 if self.open.begin_close() {
173 popover::reap_popup(cx, |bar: &mut Self| &mut bar.open);
174 }
175 self.highlighted = None;
176 cx.notify();
177 }
178
179 fn choose(&mut self, menu: usize, item: usize, cx: &mut Context<Self>) {
180 cx.emit(MenubarEvent::Selected { menu, item });
181 self.close(cx);
182 }
183
184 fn step_item(&mut self, delta: isize, cx: &mut Context<Self>) {
185 let Some(menu) = self.open_menu() else { return };
186 self.highlighted = menu::next_selectable(&self.menus[menu].items, self.highlighted, delta);
187 cx.notify();
188 }
189
190 fn step_menu(&mut self, delta: isize, cx: &mut Context<Self>) {
191 let Some(menu) = self.open_menu() else { return };
192 let count = self.menus.len() as isize;
193 if count == 0 {
194 return;
195 }
196 self.open
197 .open((menu as isize + delta).rem_euclid(count) as usize);
198 self.highlighted = None;
199 cx.notify();
200 }
201
202 fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
203 match (self.open_menu(), self.highlighted) {
204 (Some(menu), Some(item)) => self.choose(menu, item, cx),
205 (None, _) if !self.menus.is_empty() => self.show(0, window, cx),
209 _ => {}
210 }
211 }
212
213 fn dismiss(&mut self, _: &Dismiss, _: &mut Window, cx: &mut Context<Self>) {
214 self.close(cx);
215 }
216
217 fn card(&self, menu: usize, theme: &Theme, cx: &mut Context<Self>) -> gpui::AnyElement {
218 menu::card(
219 theme,
220 SharedString::from(format!("menu-{menu}")),
221 &self.menus[menu].items,
222 self.highlighted,
223 cx,
224 move |bar, item, _, cx| bar.choose(menu, item, cx),
225 )
226 .on_mouse_down_out(cx.listener(|bar, _, _, cx| bar.close(cx)))
227 .into_any_element()
228 }
229}
230
231pub fn menubar_title(theme: &Theme, label: impl Into<SharedString>, open: bool) -> gpui::Div {
233 let title = div()
234 .px(px(8.0))
235 .py(px(3.0))
236 .rounded(px(Theme::control_radius()))
237 .text_style(TextStyle::Body)
238 .cursor_pointer()
239 .child(label.into());
240 if open {
241 title.bg(theme.element_active).text_color(theme.text)
242 } else {
243 title
247 .text_color(theme.text_muted)
248 .hover(|s| s.bg(theme.element_hover).text_color(theme.text))
249 }
250}
251
252pub fn menubar() -> gpui::Div {
254 div().flex().flex_row().items_center().gap(px(2.0))
255}
256
257impl Focusable for Menubar {
258 fn focus_handle(&self, _: &App) -> FocusHandle {
259 self.focus_handle.clone()
260 }
261}
262
263impl Render for Menubar {
264 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
265 let theme = Theme::of(cx).clone();
266 let mounted = self.open.get().copied();
268 let closing = self.open.closing_since();
269
270 menubar()
271 .key_context(KEY_CONTEXT)
272 .track_focus(&self.focus_handle)
273 .on_action(cx.listener(|bar, _: &PrevMenu, _, cx| bar.step_menu(-1, cx)))
274 .on_action(cx.listener(|bar, _: &NextMenu, _, cx| bar.step_menu(1, cx)))
275 .on_action(cx.listener(|bar, _: &PrevItem, _, cx| bar.step_item(-1, cx)))
276 .on_action(cx.listener(|bar, _: &NextItem, _, cx| bar.step_item(1, cx)))
277 .on_action(cx.listener(Self::confirm))
278 .on_action(cx.listener(Self::dismiss))
279 .children((0..self.menus.len()).map(|menu| {
280 let down = mounted == Some(menu);
281 let card = down.then(|| self.card(menu, &theme, cx));
282 div()
283 .relative()
284 .id(SharedString::from(format!("menubar-title-{menu}")))
285 .on_mouse_down(
286 gpui::MouseButton::Left,
287 cx.listener(move |bar, _, _, _| {
288 bar.open.note_trigger_press_matching(|open| *open == menu)
289 }),
290 )
291 .on_click(cx.listener(move |bar, _, window, cx| bar.toggle(menu, window, cx)))
292 .on_hover(cx.listener(move |bar, hovered: &bool, _, cx| {
293 if *hovered {
294 bar.hover_switch(menu, cx);
295 }
296 }))
297 .child(menubar_title(&theme, self.menus[menu].title.clone(), down))
298 .when_some(card, |title, card| {
299 title.child(popover::anchored_menu_below(
300 SharedString::from(format!("menubar-menu-{menu}")),
301 card,
302 closing,
303 ))
304 })
305 }))
306 }
307}