1use gpui::{
42 AppContext as _, Context, Entity, EventEmitter, InteractiveElement, IntoElement, KeyDownEvent,
43 ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, div,
44};
45use gpui_kit_semantics::{NodeSpec, Role, Semantic};
46use gpui_kit_theme::{ActiveTheme, ControlSize, Space};
47
48use crate::controls::button::{Button, ButtonVariant};
49use crate::foundation::direction::{ActiveDirection, DirectionalExt};
50use crate::foundation::stepping::bounded_step;
51use crate::foundation::{Disableable, Ident, Sizable, StyledExt};
52use crate::overlay::menu::{Menu, MenuEvent, MenuItem};
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct MenubarMenu {
57 id: SharedString,
58 label: SharedString,
59 items: Vec<MenuItem>,
60 disabled: bool,
61}
62
63impl MenubarMenu {
64 pub fn new(
65 id: impl Into<SharedString>,
66 label: impl Into<SharedString>,
67 items: impl IntoIterator<Item = MenuItem>,
68 ) -> Self {
69 Self {
70 id: id.into(),
71 label: label.into(),
72 items: items.into_iter().collect(),
73 disabled: false,
74 }
75 }
76
77 pub fn disabled(mut self, disabled: bool) -> Self {
80 self.disabled = disabled;
81 self
82 }
83
84 pub fn id(&self) -> &SharedString {
85 &self.id
86 }
87
88 pub fn label(&self) -> &SharedString {
89 &self.label
90 }
91
92 pub fn is_disabled(&self) -> bool {
93 self.disabled
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum MenubarEvent {
100 Opened(SharedString),
101 Invoked {
104 menu: SharedString,
105 item: SharedString,
106 },
107 Closed(SharedString),
108}
109
110impl EventEmitter<MenubarEvent> for Menubar {}
111
112pub struct Menubar {
114 ident: Ident,
115 menus: Vec<MenubarMenu>,
116 views: Vec<Option<Entity<Menu>>>,
119 open: Option<SharedString>,
120 size: ControlSize,
121}
122
123impl std::fmt::Debug for Menubar {
124 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 formatter
126 .debug_struct("Menubar")
127 .field("ident", &self.ident)
128 .field("menus", &self.menus.len())
129 .field("open", &self.open)
130 .finish()
131 }
132}
133
134impl Menubar {
135 pub fn new(
136 ident: impl Into<Ident>,
137 menus: impl IntoIterator<Item = MenubarMenu>,
138 window: &mut Window,
139 cx: &mut Context<Self>,
140 ) -> Self {
141 let ident = ident.into();
142 let menus: Vec<MenubarMenu> = menus.into_iter().collect();
143 let mut bar = Self {
144 ident,
145 menus,
146 views: Vec::new(),
147 open: None,
148 size: ControlSize::Sm,
149 };
150 bar.build_views(window, cx);
151 bar
152 }
153
154 pub fn control_size(mut self, size: ControlSize) -> Self {
155 self.size = size;
156 self
157 }
158
159 pub fn open_menu(&self) -> Option<&SharedString> {
161 self.open.as_ref()
162 }
163
164 pub fn menus(&self) -> &[MenubarMenu] {
165 &self.menus
166 }
167
168 pub fn set_menus(
170 &mut self,
171 menus: Vec<MenubarMenu>,
172 window: &mut Window,
173 cx: &mut Context<Self>,
174 ) {
175 self.close(window, cx);
176 self.menus = menus;
177 self.build_views(window, cx);
178 cx.notify();
179 }
180
181 fn build_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
182 let ident = self.ident.clone();
183 let size = self.size;
184 let menus = self.menus.clone();
185 let mut views = Vec::with_capacity(menus.len());
186 for menu in &menus {
187 if menu.disabled {
188 views.push(None);
189 continue;
190 }
191 let id = menu.id.clone();
192 let view = cx.new(|cx| {
193 Menu::new(ident.child(id.as_ref()), window, cx)
194 .trigger(menu.label.clone())
195 .trigger_variant(ButtonVariant::Ghost)
196 .control_size(size)
197 .items(menu.items.clone())
198 });
199 cx.subscribe(&view, {
200 let id = id.clone();
201 move |bar, _, event: &MenuEvent, cx| bar.on_menu_event(&id, event, cx)
202 })
203 .detach();
204 views.push(Some(view));
205 }
206 self.views = views;
207 }
208
209 fn on_menu_event(&mut self, id: &SharedString, event: &MenuEvent, cx: &mut Context<Self>) {
210 match event {
211 MenuEvent::Opened => {
212 self.open = Some(id.clone());
213 cx.emit(MenubarEvent::Opened(id.clone()));
214 cx.notify();
215 }
216 MenuEvent::Closed => {
217 if self.open.as_ref() == Some(id) {
221 self.open = None;
222 }
223 cx.emit(MenubarEvent::Closed(id.clone()));
224 cx.notify();
225 }
226 MenuEvent::Invoked(item) => cx.emit(MenubarEvent::Invoked {
227 menu: id.clone(),
228 item: item.clone(),
229 }),
230 MenuEvent::Dismissed => {}
231 }
232 }
233
234 fn index_of(&self, id: &SharedString) -> Option<usize> {
235 self.menus.iter().position(|menu| &menu.id == id)
236 }
237
238 fn view_for(&self, id: &SharedString) -> Option<&Entity<Menu>> {
239 self.index_of(id)
240 .and_then(|index| self.views[index].as_ref())
241 }
242
243 pub fn open(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
251 let wanted = SharedString::from(id.to_string());
252 let Some(view) = self.view_for(&wanted).cloned() else {
253 return;
254 };
255 if let Some(open) = self.open.clone().filter(|open| open != &wanted) {
256 self.close_menu(&open, window, cx);
257 }
258 view.update(cx, |menu, cx| menu.open(window, cx));
259 }
260
261 pub fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
262 let Some(open) = self.open.clone() else {
263 return;
264 };
265 self.close_menu(&open, window, cx);
266 }
267
268 fn close_menu(&mut self, id: &SharedString, window: &mut Window, cx: &mut Context<Self>) {
269 let Some(view) = self.view_for(id).cloned() else {
270 return;
271 };
272 view.update(cx, |menu, cx| menu.close(window, cx));
273 }
274
275 fn on_hover_title(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
279 let Some(open) = self.open.clone() else {
280 return;
281 };
282 let Some(menu) = self.menus.get(index) else {
283 return;
284 };
285 if menu.disabled || menu.id == open {
286 return;
287 }
288 let id = menu.id.clone();
289 self.open(id.as_ref(), window, cx);
290 }
291
292 fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
298 let Some(open) = self.open.clone() else {
299 return;
300 };
301 let Some(step) = cx
302 .layout_direction()
303 .arrow_step(event.keystroke.key.as_str())
304 else {
305 return;
306 };
307 let from = self.index_of(&open);
308 let refused = |index: usize| self.menus[index].disabled;
309 let Some(next) = bounded_step(self.menus.len(), from, step as isize, refused) else {
310 return;
311 };
312 let id = self.menus[next].id.clone();
313 self.open(id.as_ref(), window, cx);
314 cx.stop_propagation();
315 }
316}
317
318impl Sizable for Menubar {
319 fn control_size(mut self, size: ControlSize) -> Self {
320 self.size = size;
321 self
322 }
323}
324
325impl Render for Menubar {
326 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
327 let theme = cx.theme().clone();
328 let direction = cx.layout_direction();
329 let bar_id = self.ident.semantic_id();
330
331 let titles = self
332 .menus
333 .iter()
334 .enumerate()
335 .map(|(index, menu)| {
336 let ident = self.ident.child(menu.id.as_ref());
337 match &self.views[index] {
338 Some(view) => div()
339 .id(ident.child("title").element_id())
340 .flex()
341 .flex_none()
342 .on_hover(cx.listener(move |bar, hovered: &bool, window, cx| {
343 if *hovered {
344 bar.on_hover_title(index, window, cx);
345 }
346 }))
347 .child(view.clone())
348 .into_any_element(),
349 None => Button::new(ident)
352 .label(menu.label.clone())
353 .ghost()
354 .control_size(self.size)
355 .disabled(true)
356 .semantic_parent(bar_id.clone())
357 .into_any_element(),
358 }
359 })
360 .collect::<Vec<_>>();
361
362 div()
363 .id(self.ident.element_id())
364 .row_reading(direction)
365 .flex_none()
366 .gap_token(&theme, Space::Xs)
367 .on_key_down(cx.listener(Self::on_key))
368 .children(titles)
369 .semantic_in(
370 cx,
371 NodeSpec::new(bar_id, Role::Toolbar).expanded(self.open.is_some()),
372 )
373 }
374}