use gpui::{
AppContext as _, Context, Entity, EventEmitter, InteractiveElement, IntoElement, KeyDownEvent,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, div,
};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, ControlSize, Space};
use crate::controls::button::{Button, ButtonVariant};
use crate::foundation::direction::{ActiveDirection, DirectionalExt};
use crate::foundation::stepping::bounded_step;
use crate::foundation::{Disableable, Ident, Sizable, StyledExt};
use crate::overlay::menu::{Menu, MenuEvent, MenuItem};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MenubarMenu {
id: SharedString,
label: SharedString,
items: Vec<MenuItem>,
disabled: bool,
}
impl MenubarMenu {
pub fn new(
id: impl Into<SharedString>,
label: impl Into<SharedString>,
items: impl IntoIterator<Item = MenuItem>,
) -> Self {
Self {
id: id.into(),
label: label.into(),
items: items.into_iter().collect(),
disabled: false,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn id(&self) -> &SharedString {
&self.id
}
pub fn label(&self) -> &SharedString {
&self.label
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MenubarEvent {
Opened(SharedString),
Invoked {
menu: SharedString,
item: SharedString,
},
Closed(SharedString),
}
impl EventEmitter<MenubarEvent> for Menubar {}
pub struct Menubar {
ident: Ident,
menus: Vec<MenubarMenu>,
views: Vec<Option<Entity<Menu>>>,
open: Option<SharedString>,
size: ControlSize,
}
impl std::fmt::Debug for Menubar {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Menubar")
.field("ident", &self.ident)
.field("menus", &self.menus.len())
.field("open", &self.open)
.finish()
}
}
impl Menubar {
pub fn new(
ident: impl Into<Ident>,
menus: impl IntoIterator<Item = MenubarMenu>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let ident = ident.into();
let menus: Vec<MenubarMenu> = menus.into_iter().collect();
let mut bar = Self {
ident,
menus,
views: Vec::new(),
open: None,
size: ControlSize::Sm,
};
bar.build_views(window, cx);
bar
}
pub fn control_size(mut self, size: ControlSize) -> Self {
self.size = size;
self
}
pub fn open_menu(&self) -> Option<&SharedString> {
self.open.as_ref()
}
pub fn menus(&self) -> &[MenubarMenu] {
&self.menus
}
pub fn set_menus(
&mut self,
menus: Vec<MenubarMenu>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.close(window, cx);
self.menus = menus;
self.build_views(window, cx);
cx.notify();
}
fn build_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let ident = self.ident.clone();
let size = self.size;
let menus = self.menus.clone();
let mut views = Vec::with_capacity(menus.len());
for menu in &menus {
if menu.disabled {
views.push(None);
continue;
}
let id = menu.id.clone();
let view = cx.new(|cx| {
Menu::new(ident.child(id.as_ref()), window, cx)
.trigger(menu.label.clone())
.trigger_variant(ButtonVariant::Ghost)
.control_size(size)
.items(menu.items.clone())
});
cx.subscribe(&view, {
let id = id.clone();
move |bar, _, event: &MenuEvent, cx| bar.on_menu_event(&id, event, cx)
})
.detach();
views.push(Some(view));
}
self.views = views;
}
fn on_menu_event(&mut self, id: &SharedString, event: &MenuEvent, cx: &mut Context<Self>) {
match event {
MenuEvent::Opened => {
self.open = Some(id.clone());
cx.emit(MenubarEvent::Opened(id.clone()));
cx.notify();
}
MenuEvent::Closed => {
if self.open.as_ref() == Some(id) {
self.open = None;
}
cx.emit(MenubarEvent::Closed(id.clone()));
cx.notify();
}
MenuEvent::Invoked(item) => cx.emit(MenubarEvent::Invoked {
menu: id.clone(),
item: item.clone(),
}),
MenuEvent::Dismissed => {}
}
}
fn index_of(&self, id: &SharedString) -> Option<usize> {
self.menus.iter().position(|menu| &menu.id == id)
}
fn view_for(&self, id: &SharedString) -> Option<&Entity<Menu>> {
self.index_of(id)
.and_then(|index| self.views[index].as_ref())
}
pub fn open(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
let wanted = SharedString::from(id.to_string());
let Some(view) = self.view_for(&wanted).cloned() else {
return;
};
if let Some(open) = self.open.clone().filter(|open| open != &wanted) {
self.close_menu(&open, window, cx);
}
view.update(cx, |menu, cx| menu.open(window, cx));
}
pub fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(open) = self.open.clone() else {
return;
};
self.close_menu(&open, window, cx);
}
fn close_menu(&mut self, id: &SharedString, window: &mut Window, cx: &mut Context<Self>) {
let Some(view) = self.view_for(id).cloned() else {
return;
};
view.update(cx, |menu, cx| menu.close(window, cx));
}
fn on_hover_title(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(open) = self.open.clone() else {
return;
};
let Some(menu) = self.menus.get(index) else {
return;
};
if menu.disabled || menu.id == open {
return;
}
let id = menu.id.clone();
self.open(id.as_ref(), window, cx);
}
fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
let Some(open) = self.open.clone() else {
return;
};
let Some(step) = cx
.layout_direction()
.arrow_step(event.keystroke.key.as_str())
else {
return;
};
let from = self.index_of(&open);
let refused = |index: usize| self.menus[index].disabled;
let Some(next) = bounded_step(self.menus.len(), from, step as isize, refused) else {
return;
};
let id = self.menus[next].id.clone();
self.open(id.as_ref(), window, cx);
cx.stop_propagation();
}
}
impl Sizable for Menubar {
fn control_size(mut self, size: ControlSize) -> Self {
self.size = size;
self
}
}
impl Render for Menubar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().clone();
let direction = cx.layout_direction();
let bar_id = self.ident.semantic_id();
let titles = self
.menus
.iter()
.enumerate()
.map(|(index, menu)| {
let ident = self.ident.child(menu.id.as_ref());
match &self.views[index] {
Some(view) => div()
.id(ident.child("title").element_id())
.flex()
.flex_none()
.on_hover(cx.listener(move |bar, hovered: &bool, window, cx| {
if *hovered {
bar.on_hover_title(index, window, cx);
}
}))
.child(view.clone())
.into_any_element(),
None => Button::new(ident)
.label(menu.label.clone())
.ghost()
.control_size(self.size)
.disabled(true)
.semantic_parent(bar_id.clone())
.into_any_element(),
}
})
.collect::<Vec<_>>();
div()
.id(self.ident.element_id())
.row_reading(direction)
.flex_none()
.gap_token(&theme, Space::Xs)
.on_key_down(cx.listener(Self::on_key))
.children(titles)
.semantic_in(
cx,
NodeSpec::new(bar_id, Role::Toolbar).expanded(self.open.is_some()),
)
}
}