linux/x11: Fix gap when tiling windows side by side (#13859)

Thorsten Ball created

By leveraging the `_GTK_EDGE_CONSTRAINTS` atom we can get all four
booleans for the `Tiling` struct and figure out which side is free when
the window is tiled to half of the screen.

For the logic behind the `_GTK_EDGE_CONSTRAINTS` see:
-
https://github.com/GNOME/mutter/blob/8e9d13aa3b3baa099ca32bbcb10afb4d9eea4460/src/x11/window-x11.c#L65-L75
-
https://github.com/GNOME/mutter/blob/8e9d13aa3b3baa099ca32bbcb10afb4d9eea4460/src/x11/window-x11.c#L1205-L1231

(I used Claude 3.5 Sonnet with our code and these pieces from `mutter`
to generate the Rust code, that was pretty sweet)

This fixes the gap in the middle when a GPUI window is tiled to the left
and another window to the right.

It's not _perfect_ but it looks a lot better.

Here's a diff that makes it look better:

```diff
diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs
index 122231f6b..7fa29dadc 100644
--- a/crates/gpui/examples/window_shadow.rs
+++ b/crates/gpui/examples/window_shadow.rs
@@ -72,8 +72,8 @@ impl Render for WindowShadow {
                     .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
                     .when(!tiling.top, |div| div.pt(shadow_size))
                     .when(!tiling.bottom, |div| div.pb(shadow_size))
-                    .when(!tiling.left, |div| div.pl(shadow_size))
-                    .when(!tiling.right, |div| div.pr(shadow_size))
+                    .when(!tiling.left, |div| div.pl(shadow_size - border_size))
+                    .when(!tiling.right, |div| div.pr(shadow_size - border_size))
                     .on_mouse_move(|_e, cx| cx.refresh())
                     .on_mouse_down(MouseButton::Left, move |e, cx| {
                         let size = cx.window_bounds().get_bounds().size;
```

But that makes it look weird on Wayland, so I didn't do it.

I think it's fine for now. Chromium looks bad and has a gap, so we're
already better.

## Before

![before_1](https://github.com/zed-industries/zed/assets/1185253/875c5cdd-c0be-4295-beb0-bb9ba5beaa52)


![before_2](https://github.com/zed-industries/zed/assets/1185253/0b96be70-4c34-4e99-aeb2-ab741171ad14)

## After

![after_1](https://github.com/zed-industries/zed/assets/1185253/aa51da77-daf1-4ef8-a33f-a83731e0c7e1)

![after_2](https://github.com/zed-industries/zed/assets/1185253/8ce7902d-90b6-4f06-ba2c-626e643abe56)


Release Notes:

- N/A

Change summary

crates/gpui/src/platform/linux/x11/window.rs | 114 ++++++++++++++++++---
crates/workspace/src/workspace.rs            |   2 
2 files changed, 99 insertions(+), 17 deletions(-)

Detailed changes

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

@@ -86,6 +86,49 @@ impl ResizeEdge {
     }
 }
 
+#[derive(Debug)]
+struct EdgeConstraints {
+    top_tiled: bool,
+    #[allow(dead_code)]
+    top_resizable: bool,
+
+    right_tiled: bool,
+    #[allow(dead_code)]
+    right_resizable: bool,
+
+    bottom_tiled: bool,
+    #[allow(dead_code)]
+    bottom_resizable: bool,
+
+    left_tiled: bool,
+    #[allow(dead_code)]
+    left_resizable: bool,
+}
+
+impl EdgeConstraints {
+    fn from_atom(atom: u32) -> Self {
+        EdgeConstraints {
+            top_tiled: (atom & (1 << 0)) != 0,
+            top_resizable: (atom & (1 << 1)) != 0,
+            right_tiled: (atom & (1 << 2)) != 0,
+            right_resizable: (atom & (1 << 3)) != 0,
+            bottom_tiled: (atom & (1 << 4)) != 0,
+            bottom_resizable: (atom & (1 << 5)) != 0,
+            left_tiled: (atom & (1 << 6)) != 0,
+            left_resizable: (atom & (1 << 7)) != 0,
+        }
+    }
+
+    fn to_tiling(&self) -> Tiling {
+        Tiling {
+            top: self.top_tiled,
+            right: self.right_tiled,
+            bottom: self.bottom_tiled,
+            left: self.left_tiled,
+        }
+    }
+}
+
 #[derive(Debug)]
 struct Visual {
     id: xproto::Visualid,
@@ -198,6 +241,7 @@ pub struct X11WindowState {
     active: bool,
     fullscreen: bool,
     decorations: WindowDecorations,
+    edge_constraints: Option<EdgeConstraints>,
     pub handle: AnyWindowHandle,
     last_insets: [u32; 4],
 }
@@ -497,6 +541,7 @@ impl X11WindowState {
             destroyed: false,
             decorations: WindowDecorations::Server,
             last_insets: [0, 0, 0, 0],
+            edge_constraints: None,
             counter_id: sync_request_counter,
             last_sync_counter: None,
             refresh_rate,
@@ -686,10 +731,32 @@ impl X11WindowStatePtr {
 
     pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) {
         let mut state = self.state.borrow_mut();
-        if event.atom == state.atoms._NET_WM_STATE
-            || event.atom == state.atoms._GTK_EDGE_CONSTRAINTS
-        {
+        if event.atom == state.atoms._NET_WM_STATE {
             self.set_wm_properties(state);
+        } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS {
+            self.set_edge_constraints(state);
+        }
+    }
+
+    fn set_edge_constraints(&self, mut state: std::cell::RefMut<X11WindowState>) {
+        let reply = self
+            .xcb_connection
+            .get_property(
+                false,
+                self.x_window,
+                state.atoms._GTK_EDGE_CONSTRAINTS,
+                xproto::AtomEnum::CARDINAL,
+                0,
+                4,
+            )
+            .unwrap()
+            .reply()
+            .unwrap();
+
+        if reply.value_len != 0 {
+            let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap());
+            let edge_constraints = EdgeConstraints::from_atom(atom);
+            state.edge_constraints.replace(edge_constraints);
         }
     }
 
@@ -1194,15 +1261,19 @@ impl PlatformWindow for X11Window {
         match state.decorations {
             WindowDecorations::Server => Decorations::Server,
             WindowDecorations::Client => {
-                // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
-                Decorations::Client {
-                    tiling: Tiling {
+                let tiling = if let Some(edge_constraints) = &state.edge_constraints {
+                    edge_constraints.to_tiling()
+                } else {
+                    // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
+                    Tiling {
                         top: state.maximized_vertical,
                         bottom: state.maximized_vertical,
                         left: state.maximized_horizontal,
                         right: state.maximized_horizontal,
-                    },
-                }
+                    }
+                };
+
+                Decorations::Client { tiling }
             }
         }
     }
@@ -1212,17 +1283,26 @@ impl PlatformWindow for X11Window {
 
         let dp = (inset.0 * state.scale_factor) as u32;
 
-        let (left, right) = if state.maximized_horizontal {
-            (0, 0)
-        } else {
-            (dp, dp)
-        };
-        let (top, bottom) = if state.maximized_vertical {
-            (0, 0)
+        let insets = if let Some(edge_constraints) = &state.edge_constraints {
+            let left = if edge_constraints.left_tiled { 0 } else { dp };
+            let top = if edge_constraints.top_tiled { 0 } else { dp };
+            let right = if edge_constraints.right_tiled { 0 } else { dp };
+            let bottom = if edge_constraints.bottom_tiled { 0 } else { dp };
+
+            [left, right, top, bottom]
         } else {
-            (dp, dp)
+            let (left, right) = if state.maximized_horizontal {
+                (0, 0)
+            } else {
+                (dp, dp)
+            };
+            let (top, bottom) = if state.maximized_vertical {
+                (0, 0)
+            } else {
+                (dp, dp)
+            };
+            [left, right, top, bottom]
         };
-        let insets = [left, right, top, bottom];
 
         if state.last_insets != insets {
             state.last_insets = insets;

crates/workspace/src/workspace.rs 🔗

@@ -6491,6 +6491,8 @@ pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext
         cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
     }
 
+    println!("decorations: {:?}", decorations);
+
     struct GlobalResizeEdge(ResizeEdge);
     impl Global for GlobalResizeEdge {}