gpui: Add support for floating windows (#39702)

Alvaro Parker created

Closes #ISSUE

This allows new windows like the Rules library or the Settings UI window
to appear floating on window managers like hyprland:


https://github.com/user-attachments/assets/628db7f9-4459-4601-85f1-789923831182

Left is with `WindowKind::Floating` and right is with
`WindowKind::Normal`

Release Notes:

- Added support for floating windows on x11 and wayland

Change summary

crates/gpui/src/platform.rs                      |  3 +
crates/gpui/src/platform/linux/wayland/client.rs |  3 +
crates/gpui/src/platform/linux/wayland/window.rs | 16 ++++++
crates/gpui/src/platform/linux/x11/client.rs     |  5 ++
crates/gpui/src/platform/linux/x11/window.rs     | 38 ++++++++++++++++++
crates/gpui/src/platform/mac/window.rs           |  4 
crates/rules_library/src/rules_library.rs        |  1 
crates/settings_ui/src/settings_ui.rs            |  2 
8 files changed, 67 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/src/platform.rs 🔗

@@ -1268,6 +1268,9 @@ pub enum WindowKind {
     /// A window that appears above all other windows, usually used for alerts or popups
     /// use sparingly!
     PopUp,
+
+    /// A floating window that appears on top of its parent window
+    Floating,
 }
 
 /// The appearance of the window, as defined by the operating system.

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

@@ -695,6 +695,8 @@ impl LinuxClient for WaylandClient {
     ) -> anyhow::Result<Box<dyn PlatformWindow>> {
         let mut state = self.0.borrow_mut();
 
+        let parent = state.keyboard_focused_window.as_ref().map(|w| w.toplevel());
+
         let (window, surface_id) = WaylandWindow::new(
             handle,
             state.globals.clone(),
@@ -702,6 +704,7 @@ impl LinuxClient for WaylandClient {
             WaylandClientStatePtr(Rc::downgrade(&self.0)),
             params,
             state.common.appearance,
+            parent,
         )?;
         state.windows.insert(surface_id, window.0.clone());
 

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

@@ -14,14 +14,16 @@ use raw_window_handle as rwh;
 use wayland_backend::client::ObjectId;
 use wayland_client::WEnum;
 use wayland_client::{Proxy, protocol::wl_surface};
-use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
 use wayland_protocols::wp::viewporter::client::wp_viewport;
 use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
 use wayland_protocols::xdg::shell::client::xdg_surface;
 use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
+use wayland_protocols::{
+    wp::fractional_scale::v1::client::wp_fractional_scale_v1,
+    xdg::shell::client::xdg_toplevel::XdgToplevel,
+};
 use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
 
-use crate::scene::Scene;
 use crate::{
     AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
     PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
@@ -36,6 +38,7 @@ use crate::{
         linux::wayland::{display::WaylandDisplay, serial::SerialKind},
     },
 };
+use crate::{WindowKind, scene::Scene};
 
 #[derive(Default)]
 pub(crate) struct Callbacks {
@@ -276,6 +279,7 @@ impl WaylandWindow {
         client: WaylandClientStatePtr,
         params: WindowParams,
         appearance: WindowAppearance,
+        parent: Option<XdgToplevel>,
     ) -> anyhow::Result<(Self, ObjectId)> {
         let surface = globals.compositor.create_surface(&globals.qh, ());
         let xdg_surface = globals
@@ -283,6 +287,10 @@ impl WaylandWindow {
             .get_xdg_surface(&surface, &globals.qh, surface.id());
         let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
 
+        if params.kind == WindowKind::Floating {
+            toplevel.set_parent(parent.as_ref());
+        }
+
         if let Some(size) = params.window_min_size {
             toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
         }
@@ -337,6 +345,10 @@ impl WaylandWindowStatePtr {
         self.state.borrow().surface.clone()
     }
 
+    pub fn toplevel(&self) -> xdg_toplevel::XdgToplevel {
+        self.state.borrow().toplevel.clone()
+    }
+
     pub fn ptr_eq(&self, other: &Self) -> bool {
         Rc::ptr_eq(&self.state, &other.state)
     }

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

@@ -1448,6 +1448,10 @@ impl LinuxClient for X11Client {
         params: WindowParams,
     ) -> anyhow::Result<Box<dyn PlatformWindow>> {
         let mut state = self.0.borrow_mut();
+        let parent_window = state
+            .keyboard_focused_window
+            .and_then(|focused_window| state.windows.get(&focused_window))
+            .map(|window| window.window.x_window);
         let x_window = state
             .xcb_connection
             .generate_id()
@@ -1466,6 +1470,7 @@ impl LinuxClient for X11Client {
             &state.atoms,
             state.scale_factor,
             state.common.appearance,
+            parent_window,
         )?;
         check_reply(
             || "Failed to set XdndAware property",

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

@@ -57,6 +57,7 @@ x11rb::atom_manager! {
         WM_PROTOCOLS,
         WM_DELETE_WINDOW,
         WM_CHANGE_STATE,
+        WM_TRANSIENT_FOR,
         _NET_WM_PID,
         _NET_WM_NAME,
         _NET_WM_STATE,
@@ -72,6 +73,7 @@ x11rb::atom_manager! {
         _NET_WM_MOVERESIZE,
         _NET_WM_WINDOW_TYPE,
         _NET_WM_WINDOW_TYPE_NOTIFICATION,
+        _NET_WM_WINDOW_TYPE_DIALOG,
         _NET_WM_SYNC,
         _NET_SUPPORTED,
         _MOTIF_WM_HINTS,
@@ -392,6 +394,7 @@ impl X11WindowState {
         atoms: &XcbAtoms,
         scale_factor: f32,
         appearance: WindowAppearance,
+        parent_window: Option<xproto::Window>,
     ) -> anyhow::Result<Self> {
         let x_screen_index = params
             .display_id
@@ -529,6 +532,7 @@ impl X11WindowState {
                     ),
                 )?;
             }
+
             if params.kind == WindowKind::PopUp {
                 check_reply(
                     || "X11 ChangeProperty32 setting window type for pop-up failed.",
@@ -542,6 +546,38 @@ impl X11WindowState {
                 )?;
             }
 
+            if params.kind == WindowKind::Floating {
+                if let Some(parent_window) = parent_window {
+                    // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
+                    // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
+                    // place the floating window in relation to the main window.
+                    // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
+                    check_reply(
+                        || "X11 ChangeProperty32 setting WM_TRANSIENT_FOR for floating window failed.",
+                        xcb.change_property32(
+                            xproto::PropMode::REPLACE,
+                            x_window,
+                            atoms.WM_TRANSIENT_FOR,
+                            xproto::AtomEnum::WINDOW,
+                            &[parent_window],
+                        ),
+                    )?;
+                }
+
+                // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
+                // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
+                check_reply(
+                    || "X11 ChangeProperty32 setting window type for floating window failed.",
+                    xcb.change_property32(
+                        xproto::PropMode::REPLACE,
+                        x_window,
+                        atoms._NET_WM_WINDOW_TYPE,
+                        xproto::AtomEnum::ATOM,
+                        &[atoms._NET_WM_WINDOW_TYPE_DIALOG],
+                    ),
+                )?;
+            }
+
             check_reply(
                 || "X11 ChangeProperty32 setting protocols failed.",
                 xcb.change_property32(
@@ -737,6 +773,7 @@ impl X11Window {
         atoms: &XcbAtoms,
         scale_factor: f32,
         appearance: WindowAppearance,
+        parent_window: Option<xproto::Window>,
     ) -> anyhow::Result<Self> {
         let ptr = X11WindowStatePtr {
             state: Rc::new(RefCell::new(X11WindowState::new(
@@ -752,6 +789,7 @@ impl X11Window {
                 atoms,
                 scale_factor,
                 appearance,
+                parent_window,
             )?)),
             callbacks: Rc::new(RefCell::new(Callbacks::default())),
             xcb: xcb.clone(),

crates/gpui/src/platform/mac/window.rs 🔗

@@ -618,7 +618,7 @@ impl MacWindow {
             }
 
             let native_window: id = match kind {
-                WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
+                WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
                 WindowKind::PopUp => {
                     style_mask |= NSWindowStyleMaskNonactivatingPanel;
                     msg_send![PANEL_CLASS, alloc]
@@ -776,7 +776,7 @@ impl MacWindow {
             native_window.makeFirstResponder_(native_view);
 
             match kind {
-                WindowKind::Normal => {
+                WindowKind::Normal | WindowKind::Floating => {
                     native_window.setLevel_(NSNormalWindowLevel);
                     native_window.setAcceptsMouseMovedEvents_(YES);
 

crates/rules_library/src/rules_library.rs 🔗

@@ -136,6 +136,7 @@ pub fn open_rules_library(
                     window_background: cx.theme().window_background_appearance(),
                     window_decorations: Some(window_decorations),
                     window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio
+                    kind: gpui::WindowKind::Floating,
                     ..Default::default()
                 },
                 |window, cx| {

crates/settings_ui/src/settings_ui.rs 🔗

@@ -484,7 +484,7 @@ pub fn open_settings_editor(
                 }),
                 focus: true,
                 show: true,
-                kind: gpui::WindowKind::Normal,
+                kind: gpui::WindowKind::Floating,
                 window_background: cx.theme().window_background_appearance(),
                 window_min_size: Some(size(px(900.), px(750.))), // 4:3 Aspect Ratio
                 window_bounds: Some(WindowBounds::centered(size(px(900.), px(750.)), cx)),