This commit is contained in:
Nihal Kumar 2026-05-31 11:47:46 +02:00 committed by GitHub
commit bc442f3044
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1628 additions and 16 deletions

16
Cargo.lock generated
View file

@ -8189,6 +8189,7 @@ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
"ashpd", "ashpd",
"async-channel 2.5.0",
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"calloop", "calloop",
@ -8226,6 +8227,7 @@ dependencies = [
"x11-clipboard", "x11-clipboard",
"x11rb", "x11rb",
"xkbcommon", "xkbcommon",
"zbus",
"zed-scap", "zed-scap",
"zed-xim", "zed-xim",
] ]
@ -14843,18 +14845,18 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.5" version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.38.3" version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -21382,12 +21384,12 @@ dependencies = [
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.7" version = "0.31.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml 0.37.5", "quick-xml 0.39.2",
"quote", "quote",
] ]

View file

@ -1348,6 +1348,11 @@ impl App {
self.platform.compositor_name() self.platform.compositor_name()
} }
/// Returns true when the application is exporting its menus to a system-managed global menu.
pub fn is_global_menu_active(&self) -> bool {
self.platform.is_global_menu_active()
}
/// Returns the file URL of the executable with the specified name in the application bundle /// Returns the file URL of the executable with the specified name in the application bundle
pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> { pub fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
self.platform.path_for_auxiliary_executable(name) self.platform.path_for_auxiliary_executable(name)

View file

@ -208,6 +208,14 @@ pub trait Platform: 'static {
fn compositor_name(&self) -> &'static str { fn compositor_name(&self) -> &'static str {
"" ""
} }
/// Returns true when the application is exporting its menus to a system-managed global menu.
///
/// This is used by cross-platform UI code to hide the in-window menu bar when the desktop
/// environment provides an app menu (e.g. KDE Plasma's global menu).
fn is_global_menu_active(&self) -> bool {
false
}
fn app_path(&self) -> Result<PathBuf>; fn app_path(&self) -> Result<PathBuf>;
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>; fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;

View file

@ -95,6 +95,9 @@ pub enum MenuItem {
/// See [`OsAction`] for more information /// See [`OsAction`] for more information
os_action: Option<OsAction>, os_action: Option<OsAction>,
/// Whether this action is checkable
checkable: bool,
/// Whether this action is checked /// Whether this action is checked
checked: bool, checked: bool,
@ -128,6 +131,7 @@ impl MenuItem {
name: name.into(), name: name.into(),
action: Box::new(action), action: Box::new(action),
os_action: None, os_action: None,
checkable: false,
checked: false, checked: false,
disabled: false, disabled: false,
} }
@ -143,6 +147,7 @@ impl MenuItem {
name: name.into(), name: name.into(),
action: Box::new(action), action: Box::new(action),
os_action: Some(os_action), os_action: Some(os_action),
checkable: false,
checked: false, checked: false,
disabled: false, disabled: false,
} }
@ -157,12 +162,14 @@ impl MenuItem {
name, name,
action, action,
os_action, os_action,
checkable,
checked, checked,
disabled, disabled,
} => OwnedMenuItem::Action { } => OwnedMenuItem::Action {
name: name.into(), name: name.into(),
action, action,
os_action, os_action,
checkable,
checked, checked,
disabled, disabled,
}, },
@ -269,6 +276,9 @@ pub enum OwnedMenuItem {
/// See [`OsAction`] for more information /// See [`OsAction`] for more information
os_action: Option<OsAction>, os_action: Option<OsAction>,
/// Whether this action is checkable
checkable: bool,
/// Whether this action is checked /// Whether this action is checked
checked: bool, checked: bool,
@ -286,12 +296,14 @@ impl Clone for OwnedMenuItem {
name, name,
action, action,
os_action, os_action,
checkable,
checked, checked,
disabled, disabled,
} => OwnedMenuItem::Action { } => OwnedMenuItem::Action {
name: name.clone(), name: name.clone(),
action: action.boxed_clone(), action: action.boxed_clone(),
os_action: *os_action, os_action: *os_action,
checkable: *checkable,
checked: *checked, checked: *checked,
disabled: *disabled, disabled: *disabled,
}, },

View file

@ -74,6 +74,8 @@ strum.workspace = true
url.workspace = true url.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
async-channel.workspace = true
zbus = "5"
# Always used # Always used
oo7 = { version = "0.6", default-features = false, features = [ oo7 = { version = "0.6", default-features = false, features = [

View file

@ -1,3 +1,4 @@
pub mod dbusmenu;
mod dispatcher; mod dispatcher;
mod headless; mod headless;
mod keyboard; mod keyboard;

File diff suppressed because it is too large Load diff

View file

@ -123,6 +123,7 @@ pub(crate) struct LinuxCommon {
pub(crate) callbacks: PlatformHandlers, pub(crate) callbacks: PlatformHandlers,
pub(crate) signal: LoopSignal, pub(crate) signal: LoopSignal,
pub(crate) menus: Vec<OwnedMenu>, pub(crate) menus: Vec<OwnedMenu>,
pub(crate) dbus_menu_server: Option<crate::linux::dbusmenu::DBusMenuServer>,
} }
impl LinuxCommon { impl LinuxCommon {
@ -150,6 +151,7 @@ impl LinuxCommon {
callbacks, callbacks,
signal, signal,
menus: Vec::new(), menus: Vec::new(),
dbus_menu_server: None,
}; };
(common, main_receiver) (common, main_receiver)
@ -215,6 +217,15 @@ impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
self.inner.compositor_name() self.inner.compositor_name()
} }
fn is_global_menu_active(&self) -> bool {
self.inner.with_common(|common| {
common
.dbus_menu_server
.as_ref()
.is_some_and(|server| server.is_connected())
})
}
fn restart(&self, binary_path: Option<PathBuf>) { fn restart(&self, binary_path: Option<PathBuf>) {
use std::os::unix::process::CommandExt as _; use std::os::unix::process::CommandExt as _;
@ -510,9 +521,12 @@ impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
Ok(app_path) Ok(app_path)
} }
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) { fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {
self.inner.with_common(|common| { self.inner.with_common(|common| {
common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
if let Some(server) = &common.dbus_menu_server {
server.set_menus(common.menus.clone(), keymap);
}
}) })
} }

View file

@ -67,6 +67,9 @@ use wayland_protocols::{
wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
}; };
use wayland_protocols_plasma::appmenu::client::{
org_kde_kwin_appmenu, org_kde_kwin_appmenu_manager,
};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
@ -127,6 +130,7 @@ pub struct Globals {
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>, pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>, pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>, pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
pub appmenu_manager: Option<org_kde_kwin_appmenu_manager::OrgKdeKwinAppmenuManager>,
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>, pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>, pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>,
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>, pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
@ -169,6 +173,7 @@ impl Globals {
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
layer_shell: globals.bind(&qh, 1..=5, ()).ok(), layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
blur_manager: globals.bind(&qh, 1..=1, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
appmenu_manager: globals.bind(&qh, 1..=2, ()).ok(),
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(),
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
@ -263,6 +268,9 @@ pub(crate) struct WaylandClientState {
cursor: Cursor, cursor: Cursor,
pending_activation: Option<PendingActivation>, pending_activation: Option<PendingActivation>,
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>, event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
dbus_service_name: Option<String>,
dbus_menu_thread: Option<std::thread::JoinHandle<()>>,
appmenu_objects: HashMap<ObjectId, org_kde_kwin_appmenu::OrgKdeKwinAppmenu>,
pub common: LinuxCommon, pub common: LinuxCommon,
} }
@ -400,6 +408,13 @@ impl WaylandClientStatePtr {
pub fn drop_window(&self, surface_id: &ObjectId) { pub fn drop_window(&self, surface_id: &ObjectId) {
let client = self.get_client(); let client = self.get_client();
let mut state = client.borrow_mut(); let mut state = client.borrow_mut();
{
if let Some(appmenu) = state.appmenu_objects.remove(surface_id) {
appmenu.release();
}
}
let closed_window = state.windows.remove(surface_id).unwrap(); let closed_window = state.windows.remove(surface_id).unwrap();
if let Some(window) = state.mouse_focused_window.take() if let Some(window) = state.mouse_focused_window.take()
&& !window.ptr_eq(&closed_window) && !window.ptr_eq(&closed_window)
@ -478,7 +493,43 @@ pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
impl Drop for WaylandClient { impl Drop for WaylandClient {
fn drop(&mut self) { fn drop(&mut self) {
// Only shut down the D-Bus menu server when the last clone drops,
// because `WaylandClient` is cheaply cloned via `Rc` and earlier drops
// must not tear down the shared server.
if Rc::strong_count(&self.0) > 1 {
return;
}
{
let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() {
Ok(mut state) => (
state.common.dbus_menu_server.clone(),
state.dbus_menu_thread.take(),
),
Err(_) => {
log::warn!(
"Failed to borrow WaylandClient inner in Drop; DBusMenu resources may leak"
);
(None, None)
}
};
if let Some(dbus_menu_server) = dbus_menu_server {
dbus_menu_server.shutdown();
}
if let Some(thread) = dbus_menu_thread {
if let Err(error) = thread.join() {
log::error!("Failed to join DBusMenu thread: {error:?}");
}
}
}
let mut state = self.0.borrow_mut(); let mut state = self.0.borrow_mut();
for (_, appmenu) in state.appmenu_objects.drain() {
appmenu.release();
}
state.windows.clear(); state.windows.clear();
if let Some(wl_pointer) = &state.wl_pointer { if let Some(wl_pointer) = &state.wl_pointer {
@ -496,6 +547,12 @@ impl Drop for WaylandClient {
} }
} }
impl crate::linux::dbusmenu::GlobalMenuState for WaylandClientState {
fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon {
&mut self.common
}
}
const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3;
fn wl_seat_version(version: u32) -> u32 { fn wl_seat_version(version: u32) -> u32 {
@ -732,12 +789,72 @@ impl WaylandClient {
cursor, cursor,
pending_activation: None, pending_activation: None,
event_loop: Some(event_loop), event_loop: Some(event_loop),
dbus_service_name: None,
dbus_menu_thread: None,
appmenu_objects: HashMap::default(),
})); }));
WaylandSource::new(conn, event_queue) WaylandSource::new(conn, event_queue)
.insert(handle) .insert(handle)
.unwrap(); .unwrap();
// Start the DBusMenu server if the compositor supports global menus.
{
let has_appmenu = state.borrow().globals.appmenu_manager.is_some();
let enabled = match crate::linux::dbusmenu::global_menu_env_override() {
Some(true) => true,
Some(false) => false,
None => has_appmenu,
};
if enabled {
let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new();
let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id());
crate::linux::dbusmenu::setup_global_menu_sources(
&dbus_menu_server,
&state.borrow().loop_handle,
Rc::downgrade(&state),
move |state| {
let (service_name, _dbus_menu_server, appmenus) = {
let Some(service_name) = state.dbus_service_name.as_ref().cloned()
else {
return;
};
let Some(dbus_menu_server) = state.common.dbus_menu_server.clone()
else {
return;
};
let appmenus = state
.appmenu_objects
.values()
.map(|appmenu| appmenu.clone())
.collect::<Vec<_>>();
(service_name, dbus_menu_server, appmenus)
};
for appmenu in appmenus {
appmenu.set_address(
service_name.clone(),
crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(),
);
}
},
);
state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone());
state.borrow_mut().dbus_service_name = Some(service_name.clone());
let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string();
let dbus_menu_thread =
dbus_menu_server.spawn_dbus_menu_thread(service_name, object_path, None);
if let Some(thread) = dbus_menu_thread {
state.borrow_mut().dbus_menu_thread = Some(thread);
}
}
}
Self(state) Self(state)
} }
} }
@ -831,7 +948,24 @@ impl LinuxClient for WaylandClient {
parent, parent,
target_output, target_output,
)?; )?;
state.windows.insert(surface_id, window.0.clone()); state.windows.insert(surface_id.clone(), window.0.clone());
if let (Some(appmenu_manager), Some(service_name)) = (
state.globals.appmenu_manager.as_ref(),
state.dbus_service_name.as_ref(),
) {
let dbus_menu_server = state.common.dbus_menu_server.clone();
let surface = window.0.surface();
let appmenu = appmenu_manager.create(&surface, &state.globals.qh, ());
if let Some(_dbus_menu_server) = dbus_menu_server
.as_ref()
.filter(|server| server.is_connected())
{
let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string();
appmenu.set_address(service_name.clone(), object_path);
}
state.appmenu_objects.insert(surface_id, appmenu);
}
Ok(Box::new(window)) Ok(Box::new(window))
} }
@ -982,16 +1116,18 @@ impl LinuxClient for WaylandClient {
} }
fn read_from_primary(&self) -> Option<gpui::ClipboardItem> { fn read_from_primary(&self) -> Option<gpui::ClipboardItem> {
self.0.borrow_mut().clipboard.read_primary() let mut state = self.0.try_borrow_mut().ok()?;
state.clipboard.read_primary()
} }
fn read_from_clipboard(&self) -> Option<gpui::ClipboardItem> { fn read_from_clipboard(&self) -> Option<gpui::ClipboardItem> {
self.0.borrow_mut().clipboard.read() let mut state = self.0.try_borrow_mut().ok()?;
state.clipboard.read()
} }
fn active_window(&self) -> Option<AnyWindowHandle> { fn active_window(&self) -> Option<AnyWindowHandle> {
self.0 let state = self.0.try_borrow().ok()?;
.borrow_mut() state
.keyboard_focused_window .keyboard_focused_window
.as_ref() .as_ref()
.map(|window| window.handle()) .map(|window| window.handle())
@ -1155,6 +1291,10 @@ delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpF
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1); delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
delegate_noop!(
WaylandClientStatePtr: ignore org_kde_kwin_appmenu_manager::OrgKdeKwinAppmenuManager
);
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_appmenu::OrgKdeKwinAppmenu);
delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3);
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter); delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter);

View file

@ -216,6 +216,10 @@ pub struct X11ClientState {
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>, pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
pub(crate) dbus_service_name: Option<String>,
pub(crate) dbus_unique_name: Option<String>,
pub(crate) dbus_menu_thread: Option<std::thread::JoinHandle<()>>,
pub(crate) supports_xinput_gestures: bool, pub(crate) supports_xinput_gestures: bool,
pub(crate) common: LinuxCommon, pub(crate) common: LinuxCommon,
@ -302,9 +306,55 @@ impl X11ClientStatePtr {
} }
} }
impl crate::linux::dbusmenu::GlobalMenuState for X11ClientState {
fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon {
&mut self.common
}
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct X11Client(pub(crate) Rc<RefCell<X11ClientState>>); pub(crate) struct X11Client(pub(crate) Rc<RefCell<X11ClientState>>);
impl Drop for X11Client {
fn drop(&mut self) {
// Only shut down the D-Bus menu server when the last clone drops,
// because `X11Client` is cheaply cloned via `Rc` and earlier drops
// must not tear down the shared server.
if Rc::strong_count(&self.0) > 1 {
return;
}
{
let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() {
Ok(mut state) => (
state.common.dbus_menu_server.clone(),
state.dbus_menu_thread.take(),
),
Err(_) => {
log::warn!(
"Failed to borrow X11Client inner in Drop; DBusMenu resources may leak"
);
let server = self
.0
.try_borrow()
.ok()
.and_then(|state| state.common.dbus_menu_server.clone());
(server, None)
}
};
if let Some(dbus_menu_server) = dbus_menu_server {
dbus_menu_server.shutdown();
}
if let Some(thread) = dbus_menu_thread {
if let Err(error) = thread.join() {
log::error!("Failed to join DBusMenu thread: {error:?}");
}
}
}
}
}
impl X11Client { impl X11Client {
pub(crate) fn new() -> anyhow::Result<Self> { pub(crate) fn new() -> anyhow::Result<Self> {
let event_loop = EventLoop::try_new()?; let event_loop = EventLoop::try_new()?;
@ -508,7 +558,7 @@ impl X11Client {
xcb_flush(&xcb_connection); xcb_flush(&xcb_connection);
Ok(X11Client(Rc::new(RefCell::new(X11ClientState { let state = Rc::new(RefCell::new(X11ClientState {
modifiers: Modifiers::default(), modifiers: Modifiers::default(),
capslock: Capslock::default(), capslock: Capslock::default(),
last_modifiers_changed_event: Modifiers::default(), last_modifiers_changed_event: Modifiers::default(),
@ -526,7 +576,7 @@ impl X11Client {
scale_factor, scale_factor,
xkb_context, xkb_context,
xcb_connection, xcb_connection: xcb_connection,
xkb_device_id, xkb_device_id,
client_side_decorations_supported, client_side_decorations_supported,
x_root_index, x_root_index,
@ -553,12 +603,86 @@ impl X11Client {
pointer_device_states, pointer_device_states,
dbus_service_name: None,
dbus_unique_name: None,
dbus_menu_thread: None,
supports_xinput_gestures, supports_xinput_gestures,
clipboard, clipboard,
clipboard_item: None, clipboard_item: None,
xdnd_state: Xdnd::default(), xdnd_state: Xdnd::default(),
})))) }));
{
let has_appmenu = crate::linux::dbusmenu::appmenu_registrar_present();
let enabled = match crate::linux::dbusmenu::global_menu_env_override() {
Some(true) => true,
Some(false) => false,
None => has_appmenu,
};
if enabled {
let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new();
let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id());
let (unique_name_tx, unique_name_rx) = calloop::channel::channel::<String>();
crate::linux::dbusmenu::setup_global_menu_sources(
&dbus_menu_server,
&state.borrow().loop_handle,
Rc::downgrade(&state),
|_| {},
);
state
.borrow()
.loop_handle
.insert_source(unique_name_rx, {
let client = Rc::downgrade(&state);
move |event, _, _| {
if let calloop::channel::Event::Msg(unique_name) = event {
if let Some(client) = client.upgrade() {
let mut state = client.borrow_mut();
state.dbus_unique_name = Some(unique_name.clone());
let window_ids: Vec<xproto::Window> =
state.windows.keys().copied().collect();
for x_window in window_ids {
let final_path =
crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH
.to_string();
set_x11_appmenu_properties(
&state.xcb_connection,
&state.atoms,
x_window,
&unique_name,
&final_path,
);
}
xcb_flush(&state.xcb_connection);
}
}
}
})
.log_err();
state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone());
state.borrow_mut().dbus_service_name = Some(service_name.clone());
let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string();
let dbus_menu_thread = dbus_menu_server.spawn_dbus_menu_thread(
service_name,
object_path,
Some(unique_name_tx),
);
if let Some(thread) = dbus_menu_thread {
state.borrow_mut().dbus_menu_thread = Some(thread);
}
}
}
Ok(X11Client(state))
} }
pub fn process_x11_events( pub fn process_x11_events(
@ -1642,6 +1766,18 @@ impl LinuxClient for X11Client {
), ),
) )
.log_err(); .log_err();
if let Some(service_name) = x11_appmenu_service_name(&state) {
let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string();
set_x11_appmenu_properties(
&state.xcb_connection,
&state.atoms,
x_window,
&service_name,
&object_path,
);
}
xcb_flush(&state.xcb_connection); xcb_flush(&state.xcb_connection);
let window_ref = WindowRef { let window_ref = WindowRef {
@ -2275,6 +2411,45 @@ fn check_gtk_frame_extents_supported(
supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS)
} }
fn x11_appmenu_service_name(state: &X11ClientState) -> Option<String> {
state
.dbus_unique_name
.clone()
.or_else(|| state.dbus_service_name.clone())
}
fn set_x11_appmenu_properties(
xcb_connection: &XCBConnection,
atoms: &XcbAtoms,
x_window: xproto::Window,
service_name: &str,
object_path: &str,
) {
check_reply(
|| "X11 ChangeProperty for _KDE_NET_WM_APPMENU_SERVICE_NAME failed.",
xcb_connection.change_property8(
xproto::PropMode::REPLACE,
x_window,
atoms._KDE_NET_WM_APPMENU_SERVICE_NAME,
xproto::AtomEnum::STRING,
service_name.as_bytes(),
),
)
.log_err();
check_reply(
|| "X11 ChangeProperty for _KDE_NET_WM_APPMENU_OBJECT_PATH failed.",
xcb_connection.change_property8(
xproto::PropMode::REPLACE,
x_window,
atoms._KDE_NET_WM_APPMENU_OBJECT_PATH,
xproto::AtomEnum::STRING,
object_path.as_bytes(),
),
)
.log_err();
}
fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool {
atom == atoms.TEXT atom == atoms.TEXT
|| atom == atoms.STRING || atom == atoms.STRING

View file

@ -83,6 +83,8 @@ x11rb::atom_manager! {
_GTK_FRAME_EXTENTS, _GTK_FRAME_EXTENTS,
_GTK_EDGE_CONSTRAINTS, _GTK_EDGE_CONSTRAINTS,
_NET_CLIENT_LIST_STACKING, _NET_CLIENT_LIST_STACKING,
_KDE_NET_WM_APPMENU_SERVICE_NAME,
_KDE_NET_WM_APPMENU_OBJECT_PATH,
} }
} }

View file

@ -305,6 +305,7 @@ impl MacPlatform {
os_action, os_action,
checked, checked,
disabled, disabled,
..
} => { } => {
// Note that this is intentionally using earlier bindings, whereas typically // Note that this is intentionally using earlier bindings, whereas typically
// later ones take display precedence. See the discussion on // later ones take display precedence. See the discussion on

View file

@ -273,6 +273,7 @@ impl ApplicationMenu {
pub(crate) fn show_menus(cx: &mut App) -> bool { pub(crate) fn show_menus(cx: &mut App) -> bool {
TitleBarSettings::get_global(cx).show_menus TitleBarSettings::get_global(cx).show_menus
&& !cx.is_global_menu_active()
&& (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some()) && (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some())
} }