Fix floating file chooser (#39154)

Alvaro Parker and David Kleingeld created

Closes #39117 

Some window managers (example: hyprland
https://github.com/hyprwm/Hyprland/issues/11229) still won't open a
floating file chooser because they don't support the XDG foreign
protocol yet: https://wayland.app/protocols/xdg-foreign-unstable-v2

Release Notes:

- Fixed file chooser not floating

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>

Change summary

Cargo.lock                                       | 23 ++++++++++++++++-
crates/gpui/Cargo.toml                           |  2 
crates/gpui/src/platform/linux/platform.rs       | 15 +++++++++++
crates/gpui/src/platform/linux/wayland/client.rs | 15 +++++++++++
crates/gpui/src/platform/linux/x11/client.rs     | 11 ++++++++
crates/gpui/src/platform/linux/x11/window.rs     |  2 
tooling/workspace-hack/Cargo.toml                | 21 ++++++++++++++++
7 files changed, 85 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -778,6 +778,9 @@ dependencies = [
  "serde",
  "serde_repr",
  "url",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols 0.32.6",
  "zbus",
 ]
 
@@ -7062,7 +7065,7 @@ dependencies = [
  "wayland-backend",
  "wayland-client",
  "wayland-cursor",
- "wayland-protocols",
+ "wayland-protocols 0.31.2",
  "wayland-protocols-plasma",
  "windows 0.61.1",
  "windows-core 0.61.0",
@@ -18167,6 +18170,18 @@ dependencies = [
  "wayland-scanner",
 ]
 
+[[package]]
+name = "wayland-protocols"
+version = "0.32.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc"
+dependencies = [
+ "bitflags 2.9.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
 [[package]]
 name = "wayland-protocols-plasma"
 version = "0.2.0"
@@ -18176,7 +18191,7 @@ dependencies = [
  "bitflags 2.9.0",
  "wayland-backend",
  "wayland-client",
- "wayland-protocols",
+ "wayland-protocols 0.31.2",
  "wayland-scanner",
 ]
 
@@ -19425,6 +19440,7 @@ dependencies = [
  "aho-corasick",
  "anstream",
  "arrayvec",
+ "ashpd 0.11.0",
  "async-compression",
  "async-std",
  "async-tungstenite",
@@ -19597,6 +19613,8 @@ dependencies = [
  "wasmtime",
  "wasmtime-cranelift",
  "wasmtime-environ",
+ "wayland-backend",
+ "wayland-sys",
  "winapi",
  "windows-core 0.61.0",
  "windows-numerics",
@@ -19604,6 +19622,7 @@ dependencies = [
  "windows-sys 0.52.0",
  "windows-sys 0.59.0",
  "windows-sys 0.61.0",
+ "zbus_macros",
  "zeroize",
  "zvariant",
 ]

crates/gpui/Cargo.toml 🔗

@@ -43,7 +43,7 @@ wayland = [
     "blade-macros",
     "blade-util",
     "bytemuck",
-    "ashpd",
+    "ashpd/wayland",
     "cosmic-text",
     "font-kit",
     "calloop-wayland-source",

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -73,6 +73,13 @@ pub trait LinuxClient {
     fn active_window(&self) -> Option<AnyWindowHandle>;
     fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
     fn run(&self);
+
+    #[cfg(any(feature = "wayland", feature = "x11"))]
+    fn window_identifier(
+        &self,
+    ) -> impl Future<Output = Option<ashpd::WindowIdentifier>> + Send + 'static {
+        std::future::ready::<Option<ashpd::WindowIdentifier>>(None)
+    }
 }
 
 #[derive(Default)]
@@ -290,6 +297,9 @@ impl<P: LinuxClient + 'static> Platform for P {
         #[cfg(not(any(feature = "wayland", feature = "x11")))]
         let _ = (done_tx.send(Ok(None)), options);
 
+        #[cfg(any(feature = "wayland", feature = "x11"))]
+        let identifier = self.window_identifier();
+
         #[cfg(any(feature = "wayland", feature = "x11"))]
         self.foreground_executor()
             .spawn(async move {
@@ -300,6 +310,7 @@ impl<P: LinuxClient + 'static> Platform for P {
                 };
 
                 let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
+                    .identifier(identifier.await)
                     .modal(true)
                     .title(title)
                     .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str))
@@ -346,6 +357,9 @@ impl<P: LinuxClient + 'static> Platform for P {
         #[cfg(not(any(feature = "wayland", feature = "x11")))]
         let _ = (done_tx.send(Ok(None)), directory, suggested_name);
 
+        #[cfg(any(feature = "wayland", feature = "x11"))]
+        let identifier = self.window_identifier();
+
         #[cfg(any(feature = "wayland", feature = "x11"))]
         self.foreground_executor()
             .spawn({
@@ -355,6 +369,7 @@ impl<P: LinuxClient + 'static> Platform for P {
                 async move {
                     let mut request_builder =
                         ashpd::desktop::file_chooser::SaveFileRequest::default()
+                            .identifier(identifier.await)
                             .modal(true)
                             .title("Save File")
                             .current_folder(directory)

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -7,6 +7,7 @@ use std::{
     time::{Duration, Instant},
 };
 
+use ashpd::WindowIdentifier;
 use calloop::{
     EventLoop, LoopHandle,
     timer::{TimeoutAction, Timer},
@@ -858,6 +859,20 @@ impl LinuxClient for WaylandClient {
     fn compositor_name(&self) -> &'static str {
         "Wayland"
     }
+
+    fn window_identifier(&self) -> impl Future<Output = Option<WindowIdentifier>> + Send + 'static {
+        async fn inner(surface: Option<wl_surface::WlSurface>) -> Option<WindowIdentifier> {
+            if let Some(surface) = surface {
+                ashpd::WindowIdentifier::from_wayland(&surface).await
+            } else {
+                None
+            }
+        }
+
+        let client_state = self.0.borrow();
+        let active_window = client_state.keyboard_focused_window.as_ref();
+        inner(active_window.map(|aw| aw.surface()))
+    }
 }
 
 impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{Capslock, xcb_flush};
 use anyhow::{Context as _, anyhow};
+use ashpd::WindowIdentifier;
 use calloop::{
     EventLoop, LoopHandle, RegistrationToken,
     generic::{FdWrapper, Generic},
@@ -1652,6 +1653,16 @@ impl LinuxClient for X11Client {
 
         Some(handles)
     }
+
+    fn window_identifier(&self) -> impl Future<Output = Option<WindowIdentifier>> + Send + 'static {
+        let state = self.0.borrow();
+        state
+            .keyboard_focused_window
+            .and_then(|focused_window| state.windows.get(&focused_window))
+            .map(|window| window.window.x_window as u64)
+            .map(|x_window| std::future::ready(Some(WindowIdentifier::from_xid(x_window))))
+            .unwrap_or(std::future::ready(None))
+    }
 }
 
 impl X11ClientState {

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -284,7 +284,7 @@ pub(crate) struct X11WindowStatePtr {
     pub state: Rc<RefCell<X11WindowState>>,
     pub(crate) callbacks: Rc<RefCell<Callbacks>>,
     xcb: Rc<XCBConnection>,
-    x_window: xproto::Window,
+    pub(crate) x_window: xproto::Window,
 }
 
 impl rwh::HasWindowHandle for RawWindow {

tooling/workspace-hack/Cargo.toml 🔗

@@ -408,6 +408,7 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
 [target.x86_64-unknown-linux-gnu.dependencies]
 aes = { version = "0.8", default-features = false, features = ["zeroize"] }
 ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
+ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
 bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
 cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
 codespan-reporting = { version = "0.12" }
@@ -446,12 +447,15 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
+wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
+wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
 zeroize = { version = "1", features = ["zeroize_derive"] }
 zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
 
 [target.x86_64-unknown-linux-gnu.build-dependencies]
 aes = { version = "0.8", default-features = false, features = ["zeroize"] }
 ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
+ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
 bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
 cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
 codespan-reporting = { version = "0.12" }
@@ -488,12 +492,16 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
+wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
+wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
+zbus_macros = { version = "5", features = ["gvariant"] }
 zeroize = { version = "1", features = ["zeroize_derive"] }
 zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
 
 [target.aarch64-unknown-linux-gnu.dependencies]
 aes = { version = "0.8", default-features = false, features = ["zeroize"] }
 ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
+ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
 bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
 cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
 codespan-reporting = { version = "0.12" }
@@ -532,12 +540,15 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
+wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
+wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
 zeroize = { version = "1", features = ["zeroize_derive"] }
 zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
 
 [target.aarch64-unknown-linux-gnu.build-dependencies]
 aes = { version = "0.8", default-features = false, features = ["zeroize"] }
 ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
+ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
 bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
 cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
 codespan-reporting = { version = "0.12" }
@@ -574,6 +585,9 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
+wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
+wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
+zbus_macros = { version = "5", features = ["gvariant"] }
 zeroize = { version = "1", features = ["zeroize_derive"] }
 zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
 
@@ -631,6 +645,7 @@ windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", feat
 [target.x86_64-unknown-linux-musl.dependencies]
 aes = { version = "0.8", default-features = false, features = ["zeroize"] }
 ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
+ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
 bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
 cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
 codespan-reporting = { version = "0.12" }
@@ -669,12 +684,15 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
+wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
+wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
 zeroize = { version = "1", features = ["zeroize_derive"] }
 zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }
 
 [target.x86_64-unknown-linux-musl.build-dependencies]
 aes = { version = "0.8", default-features = false, features = ["zeroize"] }
 ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
+ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] }
 bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
 cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
 codespan-reporting = { version = "0.12" }
@@ -711,6 +729,9 @@ tokio-rustls = { version = "0.26", default-features = false, features = ["loggin
 tokio-socks = { version = "0.5", features = ["futures-io"] }
 tokio-stream = { version = "0.1", features = ["fs"] }
 tower = { version = "0.5", default-features = false, features = ["timeout", "util"] }
+wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] }
+wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] }
+zbus_macros = { version = "5", features = ["gvariant"] }
 zeroize = { version = "1", features = ["zeroize_derive"] }
 zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] }