use gpui::prelude::FluentBuilder; use gpui::{ anchored, deferred, div, px, App, AppContext as _, ClickEvent, Context, DismissEvent, Entity, Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, }; use crate::actions::{Cancel, SelectLeft, SelectRight}; use crate::button::{Button, ButtonVariants}; use crate::menu::PopupMenu; use crate::{h_flex, Selectable, Sizable}; const CONTEXT: &str = "AppMenuBar"; pub fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("escape", Cancel, Some(CONTEXT)), KeyBinding::new("left", SelectLeft, Some(CONTEXT)), KeyBinding::new("right", SelectRight, Some(CONTEXT)), ]); } /// The application menu bar, for Windows and Linux. pub struct AppMenuBar { menus: Vec>, selected_index: Option, } impl AppMenuBar { /// Create a new app menu bar. pub fn new(cx: &mut App) -> Entity { cx.new(|cx| { let mut this = Self { selected_index: None, menus: Vec::new(), }; this.reload(cx); this }) } /// Reload the menus from the app. pub fn reload(&mut self, cx: &mut Context) { let menu_bar = cx.entity(); self.menus = cx .get_menus() .unwrap_or_default() .iter() .enumerate() .map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), cx)) .collect(); self.selected_index = None; cx.notify(); } fn on_move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { let Some(selected_index) = self.selected_index else { return; }; let new_ix = if selected_index == 0 { self.menus.len().saturating_sub(1) } else { selected_index.saturating_sub(1) }; self.set_selected_index(Some(new_ix), window, cx); } fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { let Some(selected_index) = self.selected_index else { return; }; let new_ix = if selected_index + 1 >= self.menus.len() { 0 } else { selected_index + 1 }; self.set_selected_index(Some(new_ix), window, cx); } fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { self.set_selected_index(None, window, cx); } fn set_selected_index(&mut self, ix: Option, _: &mut Window, cx: &mut Context) { self.selected_index = ix; cx.notify(); } #[inline] fn has_activated_menu(&self) -> bool { self.selected_index.is_some() } } impl Render for AppMenuBar { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .id("app-menu-bar") .key_context(CONTEXT) .on_action(cx.listener(Self::on_move_left)) .on_action(cx.listener(Self::on_move_right)) .on_action(cx.listener(Self::on_cancel)) .size_full() .gap_x_1() .overflow_x_scroll() .children(self.menus.clone()) } } /// A menu in the menu bar. pub(super) struct AppMenu { menu_bar: Entity, ix: usize, name: SharedString, menu: OwnedMenu, popup_menu: Option>, _subscription: Option, } impl AppMenu { pub(super) fn new( ix: usize, menu: &OwnedMenu, menu_bar: Entity, cx: &mut App, ) -> Entity { let name = menu.name.clone(); cx.new(|_| Self { ix, menu_bar, name, menu: menu.clone(), popup_menu: None, _subscription: None, }) } fn build_popup_menu( &mut self, window: &mut Window, cx: &mut Context, ) -> Entity { let popup_menu = match self.popup_menu.as_ref() { None => { let items = self.menu.items.clone(); let popup_menu = PopupMenu::build(window, cx, |menu, window, cx| { menu.when_some(window.focused(cx), |this, handle| { this.action_context(handle) }) .with_menu_items(items, window, cx) }); popup_menu.read(cx).focus_handle(cx).focus(window, cx); self._subscription = Some(cx.subscribe_in(&popup_menu, window, Self::handle_dismiss)); self.popup_menu = Some(popup_menu.clone()); popup_menu } Some(menu) => menu.clone(), }; let focus_handle = popup_menu.read(cx).focus_handle(cx); if !focus_handle.contains_focused(window, cx) { focus_handle.focus(window, cx); } popup_menu } fn handle_dismiss( &mut self, _: &Entity, _: &DismissEvent, window: &mut Window, cx: &mut Context, ) { self._subscription.take(); self.popup_menu.take(); self.menu_bar.update(cx, |state, cx| { state.on_cancel(&Cancel, window, cx); }); } fn handle_trigger_click( &mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context, ) { let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix); self.menu_bar.update(cx, |state, cx| { let new_ix = if is_selected { None } else { Some(self.ix) }; state.set_selected_index(new_ix, window, cx); }); } fn handle_hover(&mut self, hovered: &bool, window: &mut Window, cx: &mut Context) { if !*hovered { return; } let has_activated_menu = self.menu_bar.read(cx).has_activated_menu(); if !has_activated_menu { return; } self.menu_bar.update(cx, |state, cx| { state.set_selected_index(Some(self.ix), window, cx); }); } } impl Render for AppMenu { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let menu_bar = self.menu_bar.read(cx); let is_selected = menu_bar.selected_index == Some(self.ix); div() .id(self.ix) .relative() .child( Button::new("menu") .small() .py_0p5() .compact() .ghost() .label(self.name.clone()) .selected(is_selected) .on_mouse_down(MouseButton::Left, |_, window, cx| { // Stop propagation to avoid dragging the window. window.prevent_default(); cx.stop_propagation(); }) .on_click(cx.listener(Self::handle_trigger_click)), ) .on_hover(cx.listener(Self::handle_hover)) .when(is_selected, |this| { this.child(deferred( anchored() .anchor(gpui::Corner::TopLeft) .snap_to_window_with_margin(px(8.)) .child( div() .size_full() .occlude() .top_1() .child(self.build_popup_menu(window, cx)), ), )) }) } }