@@ -435,6 +435,13 @@
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
}
},
+ {
+ "context": "ApplicationMenu",
+ "bindings": {
+ "left": ["app_menu::NavigateApplicationMenuInDirection", "Left"],
+ "right": ["app_menu::NavigateApplicationMenuInDirection", "Right"]
+ }
+ },
// Bindings from Sublime Text
{
"context": "Editor",
@@ -1,7 +1,19 @@
-use gpui::{OwnedMenu, OwnedMenuItem, View};
+use gpui::{impl_actions, OwnedMenu, OwnedMenuItem, View};
+use serde::Deserialize;
use smallvec::SmallVec;
use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
+impl_actions!(
+ app_menu,
+ [OpenApplicationMenu, NavigateApplicationMenuInDirection,]
+);
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct OpenApplicationMenu(String);
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct NavigateApplicationMenuInDirection(String);
+
#[derive(Clone)]
struct MenuEntry {
menu: OwnedMenu,
@@ -10,6 +22,7 @@ struct MenuEntry {
pub struct ApplicationMenu {
entries: SmallVec<[MenuEntry; 8]>,
+ pending_menu_open: Option<String>,
}
impl ApplicationMenu {
@@ -23,6 +36,7 @@ impl ApplicationMenu {
handle: PopoverMenuHandle::default(),
})
.collect(),
+ pending_menu_open: None,
}
}
@@ -62,6 +76,7 @@ impl ApplicationMenu {
fn build_menu_from_items(entry: MenuEntry, cx: &mut WindowContext) -> View<ContextMenu> {
ContextMenu::build(cx, |menu, cx| {
+ // Grab current focus handle so menu can shown items in context with the focused element
let menu = menu.when_some(cx.focused(), |menu, focused| menu.context(focused));
let sanitized_items = Self::sanitize_menu_items(entry.menu.items);
@@ -93,7 +108,6 @@ impl ApplicationMenu {
let entry = entry.clone();
// Application menu must have same ids as first menu item in standard menu
- // Hence, we generate ids based on the menu name
div()
.id(SharedString::from(format!("{}-menu-item", menu_name)))
.occlude()
@@ -144,33 +158,108 @@ impl ApplicationMenu {
.with_handle(current_handle.clone()),
)
.on_hover(move |hover_enter, cx| {
- // Skip if menu is already open to avoid focus issue
if *hover_enter && !current_handle.is_deployed() {
all_handles.iter().for_each(|h| h.hide(cx));
- // Defer to prevent focus race condition with the previously open menu
+ // We need to defer this so that this menu handle can take focus from the previous menu
let handle = current_handle.clone();
cx.defer(move |cx| handle.show(cx));
}
})
}
- pub fn is_any_deployed(&self) -> bool {
+ #[cfg(not(target_os = "macos"))]
+ pub fn open_menu(&mut self, action: &OpenApplicationMenu, _cx: &mut ViewContext<Self>) {
+ self.pending_menu_open = Some(action.0.clone());
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ pub fn navigate_menus_in_direction(
+ &mut self,
+ action: &NavigateApplicationMenuInDirection,
+ cx: &mut ViewContext<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 action.0.as_str() {
+ "Left" => {
+ if current_index == 0 {
+ self.entries.len() - 1
+ } else {
+ current_index - 1
+ }
+ }
+ "Right" => {
+ if current_index == self.entries.len() - 1 {
+ 0
+ } else {
+ current_index + 1
+ }
+ }
+ _ => return,
+ };
+
+ 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(move |_, cx| next_handle.show(cx));
+ }
+
+ pub fn all_menus_shown(&self) -> bool {
self.entries.iter().any(|entry| entry.handle.is_deployed())
+ || self.pending_menu_open.is_some()
}
}
impl Render for ApplicationMenu {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- let is_any_deployed = self.is_any_deployed();
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let all_menus_shown = self.all_menus_shown();
+
+ if let Some(pending_menu_open) = self.pending_menu_open.take() {
+ if 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
+ cx.window_context().on_next_frame(move |cx| {
+ handles_to_hide.iter().for_each(|handle| handle.hide(cx));
+ cx.defer(move |cx| handle_to_show.show(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(move |_, cx| handle_to_show.show(cx));
+ }
+ }
+ }
+
div()
+ .key_context("ApplicationMenu")
.flex()
.flex_row()
.gap_x_1()
- .when(!is_any_deployed && !self.entries.is_empty(), |this| {
+ .when(!all_menus_shown && !self.entries.is_empty(), |this| {
this.child(self.render_application_menu(&self.entries[0]))
})
- .when(is_any_deployed, |this| {
+ .when(all_menus_shown, |this| {
this.children(
self.entries
.iter()
@@ -7,6 +7,10 @@ mod window_controls;
mod stories;
use crate::application_menu::ApplicationMenu;
+
+#[cfg(not(target_os = "macos"))]
+use crate::application_menu::{NavigateApplicationMenuInDirection, OpenApplicationMenu};
+
use crate::platforms::{platform_linux, platform_mac, platform_windows};
use auto_update::AutoUpdateStatus;
use call::ActiveCall;
@@ -53,7 +57,39 @@ actions!(
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, cx| {
let item = cx.new_view(|cx| TitleBar::new("title-bar", workspace, cx));
- workspace.set_titlebar_item(item.into(), cx)
+ workspace.set_titlebar_item(item.into(), cx);
+
+ #[cfg(not(target_os = "macos"))]
+ workspace.register_action(|workspace, action: &OpenApplicationMenu, cx| {
+ if let Some(titlebar) = workspace
+ .titlebar_item()
+ .and_then(|item| item.downcast::<TitleBar>().ok())
+ {
+ titlebar.update(cx, |titlebar, cx| {
+ if let Some(ref menu) = titlebar.application_menu {
+ menu.update(cx, |menu, cx| menu.open_menu(action, cx));
+ }
+ });
+ }
+ });
+
+ #[cfg(not(target_os = "macos"))]
+ workspace.register_action(
+ |workspace, action: &NavigateApplicationMenuInDirection, cx| {
+ if let Some(titlebar) = workspace
+ .titlebar_item()
+ .and_then(|item| item.downcast::<TitleBar>().ok())
+ {
+ titlebar.update(cx, |titlebar, cx| {
+ if let Some(ref menu) = titlebar.application_menu {
+ menu.update(cx, |menu, cx| {
+ menu.navigate_menus_in_direction(action, cx)
+ });
+ }
+ });
+ }
+ },
+ );
})
.detach();
}
@@ -138,7 +174,7 @@ impl Render for TitleBar {
let mut render_project_items = true;
title_bar
.when_some(self.application_menu.clone(), |title_bar, menu| {
- render_project_items = !menu.read(cx).is_any_deployed();
+ render_project_items = !menu.read(cx).all_menus_shown();
title_bar.child(menu)
})
.when(render_project_items, |title_bar| {