324 lines · 11.3 KB · Rust
RawDownload
use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, actions};use settings::Settings; use schemars::JsonSchema;use serde::Deserialize; use smallvec::SmallVec;use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use crate::title_bar_settings::TitleBarSettings; actions!(    app_menu,    [        /// Activates the menu on the right in the client-side application menu.        ///        /// Does not apply to platform menu bars (e.g. on macOS).        ActivateMenuRight,        /// Activates the menu on the left in the client-side application menu.        ///        /// Does not apply to platform menu bars (e.g. on macOS).        ActivateMenuLeft    ]); /// Opens the named menu in the client-side application menu.////// Does not apply to platform menu bars (e.g. on macOS).#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)]#[action(namespace = app_menu)]pub struct OpenApplicationMenu(String); #[cfg(not(target_os = "macos"))]pub enum ActivateDirection {    Left,    Right,} #[derive(Clone)]struct MenuEntry {    menu: OwnedMenu,    handle: PopoverMenuHandle<ContextMenu>,} pub struct ApplicationMenu {    entries: SmallVec<[MenuEntry; 8]>,    pending_menu_open: Option<String>,} impl ApplicationMenu {    pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {        let menus = cx.get_menus().unwrap_or_default();        Self {            entries: menus                .into_iter()                .map(|menu| MenuEntry {                    menu,                    handle: PopoverMenuHandle::default(),                })                .collect(),            pending_menu_open: None,        }    }     fn sanitize_menu_items(items: Vec<OwnedMenuItem>) -> Vec<OwnedMenuItem> {        let mut cleaned = Vec::new();        let mut last_was_separator = false;         for item in items {            match item {                OwnedMenuItem::Separator => {                    if !last_was_separator {                        cleaned.push(item);                        last_was_separator = true;                    }                }                OwnedMenuItem::Submenu(submenu) => {                    // Skip empty submenus                    if !submenu.items.is_empty() {                        cleaned.push(OwnedMenuItem::Submenu(submenu));                        last_was_separator = false;                    }                }                item => {                    cleaned.push(item);                    last_was_separator = false;                }            }        }         // Remove trailing separator        if let Some(OwnedMenuItem::Separator) = cleaned.last() {            cleaned.pop();        }         cleaned    }     fn build_menu_from_items(        entry: MenuEntry,        window: &mut Window,        cx: &mut App,    ) -> Entity<ContextMenu> {        ContextMenu::build(window, cx, |menu, window, cx| {            // Grab current focus handle so menu can shown items in context with the focused element            let menu = menu.when_some(window.focused(cx), |menu, focused| menu.context(focused));            let sanitized_items = Self::sanitize_menu_items(entry.menu.items);             sanitized_items                .into_iter()                .fold(menu, |menu, item| match item {                    OwnedMenuItem::Separator => menu.separator(),                    OwnedMenuItem::Action {                        name,                        action,                        checked,                        ..                    } => menu.action_checked(name, action, checked),                    OwnedMenuItem::Submenu(submenu) => {                        submenu                            .items                            .into_iter()                            .fold(menu, |menu, item| match item {                                OwnedMenuItem::Separator => menu.separator(),                                OwnedMenuItem::Action {                                    name,                                    action,                                    checked,                                    ..                                } => menu.action_checked(name, action, checked),                                OwnedMenuItem::Submenu(_) => menu,                                OwnedMenuItem::SystemMenu(_) => {                                    // A system menu doesn't make sense in this context, so ignore it                                    menu                                }                            })                    }                    OwnedMenuItem::SystemMenu(_) => {                        // A system menu doesn't make sense in this context, so ignore it                        menu                    }                })        })    }     fn render_application_menu(&self, entry: &MenuEntry) -> impl IntoElement {        let handle = entry.handle.clone();         let menu_name = entry.menu.name.clone();        let entry = entry.clone();         // Application menu must have same ids as first menu item in standard menu        div()            .id(format!("{}-menu-item", menu_name))            .occlude()            .child(                PopoverMenu::new(format!("{}-menu-popover", menu_name))                    .menu(move |window, cx| {                        Self::build_menu_from_items(entry.clone(), window, cx).into()                    })                    .trigger_with_tooltip(                        IconButton::new(                            SharedString::from(format!("{}-menu-trigger", menu_name)),                            ui::IconName::Menu,                        )                        .style(ButtonStyle::Subtle)                        .icon_size(IconSize::Small),                        Tooltip::text("Open Application Menu"),                    )                    .with_handle(handle),            )    }     fn render_standard_menu(&self, entry: &MenuEntry) -> impl IntoElement {        let current_handle = entry.handle.clone();         let menu_name = entry.menu.name.clone();        let entry = entry.clone();         let all_handles: Vec<_> = self            .entries            .iter()            .map(|entry| entry.handle.clone())            .collect();         div()            .id(format!("{}-menu-item", menu_name))            .occlude()            .child(                PopoverMenu::new(format!("{}-menu-popover", menu_name))                    .menu(move |window, cx| {                        Self::build_menu_from_items(entry.clone(), window, cx).into()                    })                    .trigger(                        Button::new(                            SharedString::from(format!("{}-menu-trigger", menu_name)),                            menu_name,                        )                        .style(ButtonStyle::Subtle)                        .label_size(LabelSize::Small),                    )                    .with_handle(current_handle.clone()),            )            .on_hover(move |hover_enter, window, cx| {                if *hover_enter && !current_handle.is_deployed() {                    all_handles.iter().for_each(|h| h.hide(cx));                     // We need to defer this so that this menu handle can take focus from the previous menu                    let handle = current_handle.clone();                    window.defer(cx, move |window, cx| handle.show(window, cx));                }            })    }     #[cfg(not(target_os = "macos"))]    pub fn open_menu(        &mut self,        action: &OpenApplicationMenu,        _window: &mut Window,        _cx: &mut Context<Self>,    ) {        self.pending_menu_open = Some(action.0.clone());    }     #[cfg(not(target_os = "macos"))]    pub fn navigate_menus_in_direction(        &mut self,        direction: ActivateDirection,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let current_index = self            .entries            .iter()            .position(|entry| entry.handle.is_deployed());        let Some(current_index) = current_index else {            return;        };         let next_index = match direction {            ActivateDirection::Left => {                if current_index == 0 {                    self.entries.len() - 1                } else {                    current_index - 1                }            }            ActivateDirection::Right => {                if current_index == self.entries.len() - 1 {                    0                } else {                    current_index + 1                }            }        };         self.entries[current_index].handle.hide(cx);         // We need to defer this so that this menu handle can take focus from the previous menu        let next_handle = self.entries[next_index].handle.clone();        cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx));    }     pub fn all_menus_shown(&self, cx: &mut Context<Self>) -> bool {        show_menus(cx)            || self.entries.iter().any(|entry| entry.handle.is_deployed())            || self.pending_menu_open.is_some()    }} pub(crate) fn show_menus(cx: &mut App) -> bool {    TitleBarSettings::get_global(cx).show_menus        && (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some())} impl Render for ApplicationMenu {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let all_menus_shown = self.all_menus_shown(cx);         if let Some(pending_menu_open) = self.pending_menu_open.take()            && let Some(entry) = self                .entries                .iter()                .find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed())        {            let handle_to_show = entry.handle.clone();            let handles_to_hide: Vec<_> = self                .entries                .iter()                .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed())                .map(|e| e.handle.clone())                .collect();             if handles_to_hide.is_empty() {                // We need to wait for the next frame to show all menus first,                // before we can handle show/hide operations                window.on_next_frame(move |window, cx| {                    handles_to_hide.iter().for_each(|handle| handle.hide(cx));                    window.defer(cx, move |window, cx| handle_to_show.show(window, cx));                });            } else {                // Since menus are already shown, we can directly handle show/hide operations                handles_to_hide.iter().for_each(|handle| handle.hide(cx));                cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx));            }        }         div()            .key_context("ApplicationMenu")            .flex()            .flex_row()            .gap_x_1()            .when(!all_menus_shown && !self.entries.is_empty(), |this| {                this.child(self.render_application_menu(&self.entries[0]))            })            .when(all_menus_shown, |this| {                this.children(                    self.entries                        .iter()                        .map(|entry| self.render_standard_menu(entry)),                )            })    }}