Linux window decorations (#13611)

Mikayla Maki , conrad , Owen Law , apricotbucket28 , and Conrad Irwin created

This PR adds support for full client side decorations on X11 and Wayland

TODO:
- [x] Adjust GPUI APIs to expose CSD related information
- [x] Implement remaining CSD features (Resizing, window border, window
shadow)
- [x] Integrate with existing background appearance and window
transparency
- [x] Figure out how to check if the window is tiled on X11
- [x] Implement in Zed
- [x] Repeatedly maximizing and unmaximizing can panic
- [x] Resizing is strangely slow
- [x] X11 resizing and movement doesn't work for this:
https://discord.com/channels/869392257814519848/1204679850208657418/1256816908519604305
- [x] The top corner can clip with current styling
- [x] Pressing titlebar buttons doesn't work
- [x] Not showing maximize / unmaximize buttons
- [x] Noisy transparency logs / surface transparency problem
https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030
- [x] Strange offsets when dragging the project panel
https://github.com/zed-industries/zed/pull/13611#pullrequestreview-2154606261
- [x] Shadow inset with `_GTK_FRAME_EXTENTS` doesn't respect tiling on
X11 (observe by snapping an X11 window in any direction)

Release Notes:

- N/A

---------

Co-authored-by: conrad <conrad@zed.dev>
Co-authored-by: Owen Law <81528246+someone13574@users.noreply.github.com>
Co-authored-by: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/collab_ui/src/collab_ui.rs                  |   5 
crates/gpui/Cargo.toml                             |   5 
crates/gpui/examples/hello_world.rs                |   2 
crates/gpui/examples/window_positioning.rs         |   1 
crates/gpui/examples/window_shadow.rs              | 222 ++++++
crates/gpui/src/color.rs                           |  10 
crates/gpui/src/geometry.rs                        |  36 
crates/gpui/src/platform.rs                        | 124 +++
crates/gpui/src/platform/linux/platform.rs         |   4 
crates/gpui/src/platform/linux/wayland/client.rs   |   2 
crates/gpui/src/platform/linux/wayland/window.rs   | 425 +++++++++--
crates/gpui/src/platform/linux/x11/client.rs       |  12 
crates/gpui/src/platform/linux/x11/window.rs       | 397 ++++++++--
crates/gpui/src/platform/mac/platform.rs           |  16 
crates/gpui/src/platform/mac/window.rs             |  15 
crates/gpui/src/platform/test/window.rs            |  10 
crates/gpui/src/platform/windows/window.rs         |  20 
crates/gpui/src/window.rs                          |  68 +
crates/theme/src/theme.rs                          |   6 
crates/title_bar/src/platforms.rs                  |   1 
crates/title_bar/src/platforms/platform_generic.rs |  47 -
crates/title_bar/src/platforms/platform_linux.rs   |  29 
crates/title_bar/src/title_bar.rs                  |  68 +
crates/title_bar/src/window_controls.rs            |   6 
crates/workspace/src/status_bar.rs                 |  16 
crates/workspace/src/workspace.rs                  | 568 +++++++++++----
crates/zed/src/zed.rs                              |   1 
crates/zed/src/zed/linux_prompts.rs                |  53 
28 files changed, 1,631 insertions(+), 538 deletions(-)

Detailed changes

crates/collab_ui/src/collab_ui.rs 🔗

@@ -10,7 +10,7 @@ use std::{rc::Rc, sync::Arc};
 pub use collab_panel::CollabPanel;
 use gpui::{
     point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
-    WindowKind, WindowOptions,
+    WindowDecorations, WindowKind, WindowOptions,
 };
 use panel_settings::MessageEditorSettings;
 pub use panel_settings::{
@@ -63,8 +63,9 @@ fn notification_window_options(
         kind: WindowKind::PopUp,
         is_movable: false,
         display_id: Some(screen.id()),
-        window_background: WindowBackgroundAppearance::default(),
+        window_background: WindowBackgroundAppearance::Transparent,
         app_id: Some(app_id.to_owned()),
         window_min_size: None,
+        window_decorations: Some(WindowDecorations::Client),
     }
 }

crates/gpui/Cargo.toml 🔗

@@ -133,6 +133,7 @@ x11rb = { version = "0.13.0", features = [
     "xinput",
     "cursor",
     "resource_manager",
+    "sync",
 ] }
 xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
 xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
@@ -160,6 +161,10 @@ path = "examples/image/image.rs"
 name = "set_menus"
 path = "examples/set_menus.rs"
 
+[[example]]
+name = "window_shadow"
+path = "examples/window_shadow.rs"
+
 [[example]]
 name = "input"
 path = "examples/input.rs"

crates/gpui/examples/hello_world.rs 🔗

@@ -23,7 +23,7 @@ impl Render for HelloWorld {
 
 fn main() {
     App::new().run(|cx: &mut AppContext| {
-        let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
+        let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
         cx.open_window(
             WindowOptions {
                 window_bounds: Some(WindowBounds::Windowed(bounds)),

crates/gpui/examples/window_shadow.rs 🔗

@@ -0,0 +1,222 @@
+use gpui::*;
+use prelude::FluentBuilder;
+
+struct WindowShadow {}
+
+/*
+Things to do:
+1. We need a way of calculating which edge or corner the mouse is on,
+    and then dispatch on that
+2. We need to improve the shadow rendering significantly
+3. We need to implement the techniques in here in Zed
+*/
+
+impl Render for WindowShadow {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let decorations = cx.window_decorations();
+        let rounding = px(10.0);
+        let shadow_size = px(10.0);
+        let border_size = px(1.0);
+        let grey = rgb(0x808080);
+        cx.set_client_inset(shadow_size);
+
+        div()
+            .id("window-backdrop")
+            .bg(transparent_black())
+            .map(|div| match decorations {
+                Decorations::Server => div,
+                Decorations::Client { tiling, .. } => div
+                    .bg(gpui::transparent_black())
+                    .child(
+                        canvas(
+                            |_bounds, cx| {
+                                cx.insert_hitbox(
+                                    Bounds::new(
+                                        point(px(0.0), px(0.0)),
+                                        cx.window_bounds().get_bounds().size,
+                                    ),
+                                    false,
+                                )
+                            },
+                            move |_bounds, hitbox, cx| {
+                                let mouse = cx.mouse_position();
+                                let size = cx.window_bounds().get_bounds().size;
+                                let Some(edge) = resize_edge(mouse, shadow_size, size) else {
+                                    return;
+                                };
+                                cx.set_cursor_style(
+                                    match edge {
+                                        ResizeEdge::Top | ResizeEdge::Bottom => {
+                                            CursorStyle::ResizeUpDown
+                                        }
+                                        ResizeEdge::Left | ResizeEdge::Right => {
+                                            CursorStyle::ResizeLeftRight
+                                        }
+                                        ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
+                                            CursorStyle::ResizeUpLeftDownRight
+                                        }
+                                        ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
+                                            CursorStyle::ResizeUpRightDownLeft
+                                        }
+                                    },
+                                    &hitbox,
+                                );
+                            },
+                        )
+                        .size_full()
+                        .absolute(),
+                    )
+                    .when(!(tiling.top || tiling.right), |div| {
+                        div.rounded_tr(rounding)
+                    })
+                    .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))
+                    .on_mouse_move(|_e, cx| cx.refresh())
+                    .on_mouse_down(MouseButton::Left, move |e, cx| {
+                        let size = cx.window_bounds().get_bounds().size;
+                        let pos = e.position;
+
+                        match resize_edge(pos, shadow_size, size) {
+                            Some(edge) => cx.start_window_resize(edge),
+                            None => cx.start_window_move(),
+                        };
+                    }),
+            })
+            .size_full()
+            .child(
+                div()
+                    .cursor(CursorStyle::Arrow)
+                    .map(|div| match decorations {
+                        Decorations::Server => div,
+                        Decorations::Client { tiling } => div
+                            .border_color(grey)
+                            .when(!(tiling.top || tiling.right), |div| {
+                                div.rounded_tr(rounding)
+                            })
+                            .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
+                            .when(!tiling.top, |div| div.border_t(border_size))
+                            .when(!tiling.bottom, |div| div.border_b(border_size))
+                            .when(!tiling.left, |div| div.border_l(border_size))
+                            .when(!tiling.right, |div| div.border_r(border_size))
+                            .when(!tiling.is_tiled(), |div| {
+                                div.shadow(smallvec::smallvec![gpui::BoxShadow {
+                                    color: Hsla {
+                                        h: 0.,
+                                        s: 0.,
+                                        l: 0.,
+                                        a: 0.4,
+                                    },
+                                    blur_radius: shadow_size / 2.,
+                                    spread_radius: px(0.),
+                                    offset: point(px(0.0), px(0.0)),
+                                }])
+                            }),
+                    })
+                    .on_mouse_move(|_e, cx| {
+                        cx.stop_propagation();
+                    })
+                    .bg(gpui::rgb(0xCCCCFF))
+                    .size_full()
+                    .flex()
+                    .flex_col()
+                    .justify_around()
+                    .child(
+                        div().w_full().flex().flex_row().justify_around().child(
+                            div()
+                                .flex()
+                                .bg(white())
+                                .size(Length::Definite(Pixels(300.0).into()))
+                                .justify_center()
+                                .items_center()
+                                .shadow_lg()
+                                .border_1()
+                                .border_color(rgb(0x0000ff))
+                                .text_xl()
+                                .text_color(rgb(0xffffff))
+                                .child(
+                                    div()
+                                        .id("hello")
+                                        .w(px(200.0))
+                                        .h(px(100.0))
+                                        .bg(green())
+                                        .shadow(smallvec::smallvec![gpui::BoxShadow {
+                                            color: Hsla {
+                                                h: 0.,
+                                                s: 0.,
+                                                l: 0.,
+                                                a: 1.0,
+                                            },
+                                            blur_radius: px(20.0),
+                                            spread_radius: px(0.0),
+                                            offset: point(px(0.0), px(0.0)),
+                                        }])
+                                        .map(|div| match decorations {
+                                            Decorations::Server => div,
+                                            Decorations::Client { .. } => div
+                                                .on_mouse_down(MouseButton::Left, |_e, cx| {
+                                                    cx.start_window_move();
+                                                })
+                                                .on_click(|e, cx| {
+                                                    if e.down.button == MouseButton::Right {
+                                                        cx.show_window_menu(e.up.position);
+                                                    }
+                                                })
+                                                .text_color(black())
+                                                .child("this is the custom titlebar"),
+                                        }),
+                                ),
+                        ),
+                    ),
+            )
+    }
+}
+
+fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
+    let edge = if pos.y < shadow_size && pos.x < shadow_size {
+        ResizeEdge::TopLeft
+    } else if pos.y < shadow_size && pos.x > size.width - shadow_size {
+        ResizeEdge::TopRight
+    } else if pos.y < shadow_size {
+        ResizeEdge::Top
+    } else if pos.y > size.height - shadow_size && pos.x < shadow_size {
+        ResizeEdge::BottomLeft
+    } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
+        ResizeEdge::BottomRight
+    } else if pos.y > size.height - shadow_size {
+        ResizeEdge::Bottom
+    } else if pos.x < shadow_size {
+        ResizeEdge::Left
+    } else if pos.x > size.width - shadow_size {
+        ResizeEdge::Right
+    } else {
+        return None;
+    };
+    Some(edge)
+}
+
+fn main() {
+    App::new().run(|cx: &mut AppContext| {
+        let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                window_background: WindowBackgroundAppearance::Opaque,
+                window_decorations: Some(WindowDecorations::Client),
+                ..Default::default()
+            },
+            |cx| {
+                cx.new_view(|cx| {
+                    cx.observe_window_appearance(|_, cx| {
+                        cx.refresh();
+                    })
+                    .detach();
+                    WindowShadow {}
+                })
+            },
+        )
+        .unwrap();
+    });
+}

crates/gpui/src/color.rs 🔗

@@ -309,6 +309,16 @@ pub fn transparent_black() -> Hsla {
     }
 }
 
+/// Transparent black in [`Hsla`]
+pub fn transparent_white() -> Hsla {
+    Hsla {
+        h: 0.,
+        s: 0.,
+        l: 1.,
+        a: 0.,
+    }
+}
+
 /// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
 pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
     Hsla {

crates/gpui/src/geometry.rs 🔗

@@ -883,6 +883,14 @@ where
         self.size.height = self.size.height.clone() + double_amount;
     }
 
+    /// inset the bounds by a specified amount
+    /// Note that this may panic if T does not support negative values
+    pub fn inset(&self, amount: T) -> Self {
+        let mut result = self.clone();
+        result.dilate(T::default() - amount);
+        result
+    }
+
     /// Returns the center point of the bounds.
     ///
     /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height
@@ -1266,12 +1274,36 @@ where
     ///     size: Size { width: 10.0, height: 20.0 },
     /// });
     /// ```
-    pub fn map_origin(self, f: impl Fn(Point<T>) -> Point<T>) -> Bounds<T> {
+    pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds<T> {
         Bounds {
-            origin: f(self.origin),
+            origin: self.origin.map(f),
             size: self.size,
         }
     }
+
+    /// Applies a function to the origin  of the bounds, producing a new `Bounds` with the new origin
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use zed::{Bounds, Point, Size};
+    /// let bounds = Bounds {
+    ///     origin: Point { x: 10.0, y: 10.0 },
+    ///     size: Size { width: 10.0, height: 20.0 },
+    /// };
+    /// let new_bounds = bounds.map_size(|value| value * 1.5);
+    ///
+    /// assert_eq!(new_bounds, Bounds {
+    ///     origin: Point { x: 10.0, y: 10.0 },
+    ///     size: Size { width: 15.0, height: 30.0 },
+    /// });
+    /// ```
+    pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds<T> {
+        Bounds {
+            origin: self.origin,
+            size: self.size.map(f),
+        }
+    }
 }
 
 /// Checks if the bounds represent an empty area.

crates/gpui/src/platform.rs 🔗

@@ -210,6 +210,83 @@ impl Debug for DisplayId {
 
 unsafe impl Send for DisplayId {}
 
+/// Which part of the window to resize
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ResizeEdge {
+    /// The top edge
+    Top,
+    /// The top right corner
+    TopRight,
+    /// The right edge
+    Right,
+    /// The bottom right corner
+    BottomRight,
+    /// The bottom edge
+    Bottom,
+    /// The bottom left corner
+    BottomLeft,
+    /// The left edge
+    Left,
+    /// The top left corner
+    TopLeft,
+}
+
+/// A type to describe the appearance of a window
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
+pub enum WindowDecorations {
+    #[default]
+    /// Server side decorations
+    Server,
+    /// Client side decorations
+    Client,
+}
+
+/// A type to describe how this window is currently configured
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
+pub enum Decorations {
+    /// The window is configured to use server side decorations
+    #[default]
+    Server,
+    /// The window is configured to use client side decorations
+    Client {
+        /// The edge tiling state
+        tiling: Tiling,
+    },
+}
+
+/// What window controls this platform supports
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
+pub struct WindowControls {
+    /// Whether this platform supports fullscreen
+    pub fullscreen: bool,
+    /// Whether this platform supports maximize
+    pub maximize: bool,
+    /// Whether this platform supports minimize
+    pub minimize: bool,
+    /// Whether this platform supports a window menu
+    pub window_menu: bool,
+}
+
+/// A type to describe which sides of the window are currently tiled in some way
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
+pub struct Tiling {
+    /// Whether the top edge is tiled
+    pub top: bool,
+    /// Whether the left edge is tiled
+    pub left: bool,
+    /// Whether the right edge is tiled
+    pub right: bool,
+    /// Whether the bottom edge is tiled
+    pub bottom: bool,
+}
+
+impl Tiling {
+    /// Whether any edge is tiled
+    pub fn is_tiled(&self) -> bool {
+        self.top || self.left || self.right || self.bottom
+    }
+}
+
 pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn bounds(&self) -> Bounds<Pixels>;
     fn is_maximized(&self) -> bool;
@@ -232,10 +309,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn activate(&self);
     fn is_active(&self) -> bool;
     fn set_title(&mut self, title: &str);
-    fn set_app_id(&mut self, app_id: &str);
-    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
-    fn set_edited(&mut self, edited: bool);
-    fn show_character_palette(&self);
+    fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
     fn minimize(&self);
     fn zoom(&self);
     fn toggle_fullscreen(&self);
@@ -252,12 +326,31 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn completed_frame(&self) {}
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
 
+    // macOS specific methods
+    fn set_edited(&mut self, _edited: bool) {}
+    fn show_character_palette(&self) {}
+
     #[cfg(target_os = "windows")]
     fn get_raw_handle(&self) -> windows::HWND;
 
-    fn show_window_menu(&self, position: Point<Pixels>);
-    fn start_system_move(&self);
-    fn should_render_window_controls(&self) -> bool;
+    // Linux specific methods
+    fn request_decorations(&self, _decorations: WindowDecorations) {}
+    fn show_window_menu(&self, _position: Point<Pixels>) {}
+    fn start_window_move(&self) {}
+    fn start_window_resize(&self, _edge: ResizeEdge) {}
+    fn window_decorations(&self) -> Decorations {
+        Decorations::Server
+    }
+    fn set_app_id(&mut self, _app_id: &str) {}
+    fn window_controls(&self) -> WindowControls {
+        WindowControls {
+            fullscreen: true,
+            maximize: true,
+            minimize: true,
+            window_menu: false,
+        }
+    }
+    fn set_client_inset(&self, _inset: Pixels) {}
 
     #[cfg(any(test, feature = "test-support"))]
     fn as_test(&mut self) -> Option<&mut TestWindow> {
@@ -570,6 +663,10 @@ pub struct WindowOptions {
 
     /// Window minimum size
     pub window_min_size: Option<Size<Pixels>>,
+
+    /// Whether to use client or server side decorations. Wayland only
+    /// Note that this may be ignored.
+    pub window_decorations: Option<WindowDecorations>,
 }
 
 /// The variables that can be configured when creating a new window
@@ -596,8 +693,6 @@ pub(crate) struct WindowParams {
 
     pub display_id: Option<DisplayId>,
 
-    pub window_background: WindowBackgroundAppearance,
-
     #[cfg_attr(target_os = "linux", allow(dead_code))]
     pub window_min_size: Option<Size<Pixels>>,
 }
@@ -649,6 +744,7 @@ impl Default for WindowOptions {
             window_background: WindowBackgroundAppearance::default(),
             app_id: None,
             window_min_size: None,
+            window_decorations: None,
         }
     }
 }
@@ -659,7 +755,7 @@ pub struct TitlebarOptions {
     /// The initial title of the window
     pub title: Option<SharedString>,
 
-    /// Whether the titlebar should appear transparent
+    /// Whether the titlebar should appear transparent (macOS only)
     pub appears_transparent: bool,
 
     /// The position of the macOS traffic light buttons
@@ -805,6 +901,14 @@ pub enum CursorStyle {
     /// corresponds to the CSS cursor value `ns-resize`
     ResizeUpDown,
 
+    /// A resize cursor directing up-left and down-right
+    /// corresponds to the CSS cursor value `nesw-resize`
+    ResizeUpLeftDownRight,
+
+    /// A resize cursor directing up-right and down-left
+    /// corresponds to the CSS cursor value `nwse-resize`
+    ResizeUpRightDownLeft,
+
     /// A cursor indicating that the item/column can be resized horizontally.
     /// corresponds to the CSS curosr value `col-resize`
     ResizeColumn,

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

@@ -572,6 +572,8 @@ impl CursorStyle {
             CursorStyle::ResizeUp => Shape::NResize,
             CursorStyle::ResizeDown => Shape::SResize,
             CursorStyle::ResizeUpDown => Shape::NsResize,
+            CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
+            CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
             CursorStyle::ResizeColumn => Shape::ColResize,
             CursorStyle::ResizeRow => Shape::RowResize,
             CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
@@ -599,6 +601,8 @@ impl CursorStyle {
             CursorStyle::ResizeUp => "n-resize",
             CursorStyle::ResizeDown => "s-resize",
             CursorStyle::ResizeUpDown => "ns-resize",
+            CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
+            CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
             CursorStyle::ResizeColumn => "col-resize",
             CursorStyle::ResizeRow => "row-resize",
             CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",

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

@@ -138,7 +138,7 @@ impl Globals {
             primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
             shm: globals.bind(&qh, 1..=1, ()).unwrap(),
             seat,
-            wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
+            wm_base: globals.bind(&qh, 2..=5, ()).unwrap(),
             viewporter: globals.bind(&qh, 1..=1, ()).ok(),
             fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
             decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),

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

@@ -25,9 +25,10 @@ use crate::platform::linux::wayland::serial::SerialKind;
 use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
 use crate::scene::Scene;
 use crate::{
-    px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay,
-    PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance,
-    WindowBackgroundAppearance, WindowBounds, WindowParams,
+    px, size, AnyWindowHandle, Bounds, Decorations, Globals, Modifiers, Output, Pixels,
+    PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling,
+    WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
+    WindowControls, WindowDecorations, WindowParams,
 };
 
 #[derive(Default)]
@@ -62,10 +63,12 @@ impl rwh::HasDisplayHandle for RawWindow {
     }
 }
 
+#[derive(Debug)]
 struct InProgressConfigure {
     size: Option<Size<Pixels>>,
     fullscreen: bool,
     maximized: bool,
+    tiling: Tiling,
 }
 
 pub struct WaylandWindowState {
@@ -84,14 +87,20 @@ pub struct WaylandWindowState {
     bounds: Bounds<Pixels>,
     scale: f32,
     input_handler: Option<PlatformInputHandler>,
-    decoration_state: WaylandDecorationState,
+    decorations: WindowDecorations,
+    background_appearance: WindowBackgroundAppearance,
     fullscreen: bool,
     maximized: bool,
-    windowed_bounds: Bounds<Pixels>,
+    tiling: Tiling,
+    window_bounds: Bounds<Pixels>,
     client: WaylandClientStatePtr,
     handle: AnyWindowHandle,
     active: bool,
     in_progress_configure: Option<InProgressConfigure>,
+    in_progress_window_controls: Option<WindowControls>,
+    window_controls: WindowControls,
+    inset: Option<Pixels>,
+    requested_inset: Option<Pixels>,
 }
 
 #[derive(Clone)]
@@ -142,7 +151,7 @@ impl WaylandWindowState {
                 height: options.bounds.size.height.0 as u32,
                 depth: 1,
             },
-            transparent: options.window_background != WindowBackgroundAppearance::Opaque,
+            transparent: true,
         };
 
         Ok(Self {
@@ -160,17 +169,34 @@ impl WaylandWindowState {
             bounds: options.bounds,
             scale: 1.0,
             input_handler: None,
-            decoration_state: WaylandDecorationState::Client,
+            decorations: WindowDecorations::Client,
+            background_appearance: WindowBackgroundAppearance::Opaque,
             fullscreen: false,
             maximized: false,
-            windowed_bounds: options.bounds,
+            tiling: Tiling::default(),
+            window_bounds: options.bounds,
             in_progress_configure: None,
             client,
             appearance,
             handle,
             active: false,
+            in_progress_window_controls: None,
+            // Assume that we can do anything, unless told otherwise
+            window_controls: WindowControls {
+                fullscreen: true,
+                maximize: true,
+                minimize: true,
+                window_menu: true,
+            },
+            inset: None,
+            requested_inset: None,
         })
     }
+
+    pub fn is_transparent(&self) -> bool {
+        self.decorations == WindowDecorations::Client
+            || self.background_appearance != WindowBackgroundAppearance::Opaque
+    }
 }
 
 pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
@@ -235,7 +261,7 @@ impl WaylandWindow {
             .wm_base
             .get_xdg_surface(&surface, &globals.qh, surface.id());
         let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
-        toplevel.set_min_size(200, 200);
+        toplevel.set_min_size(50, 50);
 
         if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
             fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -246,13 +272,7 @@ impl WaylandWindow {
             .decoration_manager
             .as_ref()
             .map(|decoration_manager| {
-                let decoration = decoration_manager.get_toplevel_decoration(
-                    &toplevel,
-                    &globals.qh,
-                    surface.id(),
-                );
-                decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
-                decoration
+                decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
             });
 
         let viewport = globals
@@ -298,7 +318,7 @@ impl WaylandWindowStatePtr {
 
     pub fn frame(&self, request_frame_callback: bool) {
         if request_frame_callback {
-            let state = self.state.borrow_mut();
+            let mut state = self.state.borrow_mut();
             state.surface.frame(&state.globals.qh, state.surface.id());
             drop(state);
         }
@@ -311,6 +331,18 @@ impl WaylandWindowStatePtr {
     pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
         match event {
             xdg_surface::Event::Configure { serial } => {
+                {
+                    let mut state = self.state.borrow_mut();
+                    if let Some(window_controls) = state.in_progress_window_controls.take() {
+                        state.window_controls = window_controls;
+
+                        drop(state);
+                        let mut callbacks = self.callbacks.borrow_mut();
+                        if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
+                            appearance_changed();
+                        }
+                    }
+                }
                 {
                     let mut state = self.state.borrow_mut();
 
@@ -318,18 +350,21 @@ impl WaylandWindowStatePtr {
                         let got_unmaximized = state.maximized && !configure.maximized;
                         state.fullscreen = configure.fullscreen;
                         state.maximized = configure.maximized;
-
+                        state.tiling = configure.tiling;
                         if got_unmaximized {
-                            configure.size = Some(state.windowed_bounds.size);
-                        } else if !configure.fullscreen && !configure.maximized {
+                            configure.size = Some(state.window_bounds.size);
+                        } else if !configure.maximized {
+                            configure.size =
+                                compute_outer_size(state.inset, configure.size, state.tiling);
+                        }
+                        if !configure.fullscreen && !configure.maximized {
                             if let Some(size) = configure.size {
-                                state.windowed_bounds = Bounds {
+                                state.window_bounds = Bounds {
                                     origin: Point::default(),
                                     size,
                                 };
                             }
                         }
-
                         drop(state);
                         if let Some(size) = configure.size {
                             self.resize(size);
@@ -340,8 +375,11 @@ impl WaylandWindowStatePtr {
                 state.xdg_surface.ack_configure(serial);
                 let request_frame_callback = !state.acknowledged_first_configure;
                 state.acknowledged_first_configure = true;
-                drop(state);
-                self.frame(request_frame_callback);
+
+                if request_frame_callback {
+                    drop(state);
+                    self.frame(true);
+                }
             }
             _ => {}
         }
@@ -351,10 +389,21 @@ impl WaylandWindowStatePtr {
         match event {
             zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
                 WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
-                    self.set_decoration_state(WaylandDecorationState::Server)
+                    self.state.borrow_mut().decorations = WindowDecorations::Server;
+                    if let Some(mut appearance_changed) =
+                        self.callbacks.borrow_mut().appearance_changed.as_mut()
+                    {
+                        appearance_changed();
+                    }
                 }
                 WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
-                    self.set_decoration_state(WaylandDecorationState::Client)
+                    self.state.borrow_mut().decorations = WindowDecorations::Client;
+                    // Update background to be transparent
+                    if let Some(mut appearance_changed) =
+                        self.callbacks.borrow_mut().appearance_changed.as_mut()
+                    {
+                        appearance_changed();
+                    }
                 }
                 WEnum::Value(_) => {
                     log::warn!("Unknown decoration mode");
@@ -389,14 +438,44 @@ impl WaylandWindowStatePtr {
                     Some(size(px(width as f32), px(height as f32)))
                 };
 
-                let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
-                let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
+                let states = extract_states::<xdg_toplevel::State>(&states);
+
+                let mut tiling = Tiling::default();
+                let mut fullscreen = false;
+                let mut maximized = false;
+
+                for state in states {
+                    match state {
+                        xdg_toplevel::State::Maximized => {
+                            maximized = true;
+                        }
+                        xdg_toplevel::State::Fullscreen => {
+                            fullscreen = true;
+                        }
+                        xdg_toplevel::State::TiledTop => {
+                            tiling.top = true;
+                        }
+                        xdg_toplevel::State::TiledLeft => {
+                            tiling.left = true;
+                        }
+                        xdg_toplevel::State::TiledRight => {
+                            tiling.right = true;
+                        }
+                        xdg_toplevel::State::TiledBottom => {
+                            tiling.bottom = true;
+                        }
+                        _ => {
+                            // noop
+                        }
+                    }
+                }
 
                 let mut state = self.state.borrow_mut();
                 state.in_progress_configure = Some(InProgressConfigure {
                     size,
                     fullscreen,
                     maximized,
+                    tiling,
                 });
 
                 false
@@ -415,6 +494,33 @@ impl WaylandWindowStatePtr {
                     true
                 }
             }
+            xdg_toplevel::Event::WmCapabilities { capabilities } => {
+                let mut window_controls = WindowControls::default();
+
+                let states = extract_states::<xdg_toplevel::WmCapabilities>(&capabilities);
+
+                for state in states {
+                    match state {
+                        xdg_toplevel::WmCapabilities::Maximize => {
+                            window_controls.maximize = true;
+                        }
+                        xdg_toplevel::WmCapabilities::Minimize => {
+                            window_controls.minimize = true;
+                        }
+                        xdg_toplevel::WmCapabilities::Fullscreen => {
+                            window_controls.fullscreen = true;
+                        }
+                        xdg_toplevel::WmCapabilities::WindowMenu => {
+                            window_controls.window_menu = true;
+                        }
+                        _ => {}
+                    }
+                }
+
+                let mut state = self.state.borrow_mut();
+                state.in_progress_window_controls = Some(window_controls);
+                false
+            }
             _ => false,
         }
     }
@@ -545,18 +651,6 @@ impl WaylandWindowStatePtr {
         self.set_size_and_scale(None, Some(scale));
     }
 
-    /// Notifies the window of the state of the decorations.
-    ///
-    /// # Note
-    ///
-    /// This API is indirectly called by the wayland compositor and
-    /// not meant to be called by a user who wishes to change the state
-    /// of the decorations. This is because the state of the decorations
-    /// is managed by the compositor and not the client.
-    pub fn set_decoration_state(&self, state: WaylandDecorationState) {
-        self.state.borrow_mut().decoration_state = state;
-    }
-
     pub fn close(&self) {
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(fun) = callbacks.close.take() {
@@ -599,6 +693,17 @@ impl WaylandWindowStatePtr {
     }
 }
 
+fn extract_states<'a, S: TryFrom<u32> + 'a>(states: &'a [u8]) -> impl Iterator<Item = S> + 'a
+where
+    <S as TryFrom<u32>>::Error: 'a,
+{
+    states
+        .chunks_exact(4)
+        .flat_map(TryInto::<[u8; 4]>::try_into)
+        .map(u32::from_ne_bytes)
+        .flat_map(S::try_from)
+}
+
 fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
     let mut scale = 1;
     let mut current_output = state.display.take();
@@ -639,9 +744,9 @@ impl PlatformWindow for WaylandWindow {
     fn window_bounds(&self) -> WindowBounds {
         let state = self.borrow();
         if state.fullscreen {
-            WindowBounds::Fullscreen(state.windowed_bounds)
+            WindowBounds::Fullscreen(state.window_bounds)
         } else if state.maximized {
-            WindowBounds::Maximized(state.windowed_bounds)
+            WindowBounds::Maximized(state.window_bounds)
         } else {
             drop(state);
             WindowBounds::Windowed(self.bounds())
@@ -718,52 +823,10 @@ impl PlatformWindow for WaylandWindow {
         self.borrow().toplevel.set_app_id(app_id.to_owned());
     }
 
-    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
-        let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
+    fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
         let mut state = self.borrow_mut();
-        state.renderer.update_transparency(!opaque);
-
-        let region = state
-            .globals
-            .compositor
-            .create_region(&state.globals.qh, ());
-        region.add(0, 0, i32::MAX, i32::MAX);
-
-        if opaque {
-            // Promise the compositor that this region of the window surface
-            // contains no transparent pixels. This allows the compositor to
-            // do skip whatever is behind the surface for better performance.
-            state.surface.set_opaque_region(Some(&region));
-        } else {
-            state.surface.set_opaque_region(None);
-        }
-
-        if let Some(ref blur_manager) = state.globals.blur_manager {
-            if background_appearance == WindowBackgroundAppearance::Blurred {
-                if state.blur.is_none() {
-                    let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
-                    blur.set_region(Some(&region));
-                    state.blur = Some(blur);
-                }
-                state.blur.as_ref().unwrap().commit();
-            } else {
-                // It probably doesn't hurt to clear the blur for opaque windows
-                blur_manager.unset(&state.surface);
-                if let Some(b) = state.blur.take() {
-                    b.release()
-                }
-            }
-        }
-
-        region.destroy();
-    }
-
-    fn set_edited(&mut self, _edited: bool) {
-        log::info!("ignoring macOS specific set_edited");
-    }
-
-    fn show_character_palette(&self) {
-        log::info!("ignoring macOS specific show_character_palette");
+        state.background_appearance = background_appearance;
+        update_window(state);
     }
 
     fn minimize(&self) {
@@ -831,6 +894,25 @@ impl PlatformWindow for WaylandWindow {
 
     fn completed_frame(&self) {
         let mut state = self.borrow_mut();
+        if let Some(area) = state.requested_inset {
+            state.inset = Some(area);
+        }
+
+        let window_geometry = inset_by_tiling(
+            state.bounds.map_origin(|_| px(0.0)),
+            state.inset.unwrap_or(px(0.0)),
+            state.tiling,
+        )
+        .map(|v| v.0 as i32)
+        .map_size(|v| if v <= 0 { 1 } else { v });
+
+        state.xdg_surface.set_window_geometry(
+            window_geometry.origin.x,
+            window_geometry.origin.y,
+            window_geometry.size.width,
+            window_geometry.size.height,
+        );
+
         state.surface.commit();
     }
 
@@ -850,22 +932,173 @@ impl PlatformWindow for WaylandWindow {
         );
     }
 
-    fn start_system_move(&self) {
+    fn start_window_move(&self) {
         let state = self.borrow();
         let serial = state.client.get_serial(SerialKind::MousePress);
         state.toplevel._move(&state.globals.seat, serial);
     }
 
-    fn should_render_window_controls(&self) -> bool {
-        self.borrow().decoration_state == WaylandDecorationState::Client
+    fn start_window_resize(&self, edge: crate::ResizeEdge) {
+        let state = self.borrow();
+        state.toplevel.resize(
+            &state.globals.seat,
+            state.client.get_serial(SerialKind::MousePress),
+            edge.to_xdg(),
+        )
+    }
+
+    fn window_decorations(&self) -> Decorations {
+        let state = self.borrow();
+        match state.decorations {
+            WindowDecorations::Server => Decorations::Server,
+            WindowDecorations::Client => Decorations::Client {
+                tiling: state.tiling,
+            },
+        }
+    }
+
+    fn request_decorations(&self, decorations: WindowDecorations) {
+        let mut state = self.borrow_mut();
+        state.decorations = decorations;
+        if let Some(decoration) = state.decoration.as_ref() {
+            decoration.set_mode(decorations.to_xdg());
+            update_window(state);
+        }
+    }
+
+    fn window_controls(&self) -> WindowControls {
+        self.borrow().window_controls
+    }
+
+    fn set_client_inset(&self, inset: Pixels) {
+        let mut state = self.borrow_mut();
+        if Some(inset) != state.inset {
+            state.requested_inset = Some(inset);
+            update_window(state);
+        }
+    }
+}
+
+fn update_window(mut state: RefMut<WaylandWindowState>) {
+    let opaque = !state.is_transparent();
+
+    state.renderer.update_transparency(!opaque);
+    let mut opaque_area = state.window_bounds.map(|v| v.0 as i32);
+    if let Some(inset) = state.inset {
+        opaque_area.inset(inset.0 as i32);
+    }
+
+    let region = state
+        .globals
+        .compositor
+        .create_region(&state.globals.qh, ());
+    region.add(
+        opaque_area.origin.x,
+        opaque_area.origin.y,
+        opaque_area.size.width,
+        opaque_area.size.height,
+    );
+
+    // Note that rounded corners make this rectangle API hard to work with.
+    // As this is common when using CSD, let's just disable this API.
+    if state.background_appearance == WindowBackgroundAppearance::Opaque
+        && state.decorations == WindowDecorations::Server
+    {
+        // Promise the compositor that this region of the window surface
+        // contains no transparent pixels. This allows the compositor to
+        // do skip whatever is behind the surface for better performance.
+        state.surface.set_opaque_region(Some(&region));
+    } else {
+        state.surface.set_opaque_region(None);
+    }
+
+    if let Some(ref blur_manager) = state.globals.blur_manager {
+        if state.background_appearance == WindowBackgroundAppearance::Blurred {
+            if state.blur.is_none() {
+                let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
+                blur.set_region(Some(&region));
+                state.blur = Some(blur);
+            }
+            state.blur.as_ref().unwrap().commit();
+        } else {
+            // It probably doesn't hurt to clear the blur for opaque windows
+            blur_manager.unset(&state.surface);
+            if let Some(b) = state.blur.take() {
+                b.release()
+            }
+        }
+    }
+
+    region.destroy();
+}
+
+impl WindowDecorations {
+    fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode {
+        match self {
+            WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
+            WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
+        }
     }
 }
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum WaylandDecorationState {
-    /// Decorations are to be provided by the client
-    Client,
+impl ResizeEdge {
+    fn to_xdg(&self) -> xdg_toplevel::ResizeEdge {
+        match self {
+            ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
+            ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,
+            ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right,
+            ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight,
+            ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom,
+            ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft,
+            ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left,
+            ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft,
+        }
+    }
+}
+
+/// The configuration event is in terms of the window geometry, which we are constantly
+/// updating to account for the client decorations. But that's not the area we want to render
+/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets
+fn compute_outer_size(
+    inset: Option<Pixels>,
+    new_size: Option<Size<Pixels>>,
+    tiling: Tiling,
+) -> Option<Size<Pixels>> {
+    let Some(inset) = inset else { return new_size };
+
+    new_size.map(|mut new_size| {
+        if !tiling.top {
+            new_size.height += inset;
+        }
+        if !tiling.bottom {
+            new_size.height += inset;
+        }
+        if !tiling.left {
+            new_size.width += inset;
+        }
+        if !tiling.right {
+            new_size.width += inset;
+        }
+
+        new_size
+    })
+}
+
+fn inset_by_tiling(mut bounds: Bounds<Pixels>, inset: Pixels, tiling: Tiling) -> Bounds<Pixels> {
+    if !tiling.top {
+        bounds.origin.y += inset;
+        bounds.size.height -= inset;
+    }
+    if !tiling.bottom {
+        bounds.size.height -= inset;
+    }
+    if !tiling.left {
+        bounds.origin.x += inset;
+        bounds.size.width -= inset;
+    }
+    if !tiling.right {
+        bounds.size.width -= inset;
+    }
 
-    /// Decorations are provided by the server
-    Server,
+    bounds
 }

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

@@ -512,7 +512,7 @@ impl X11Client {
         match event {
             Event::ClientMessage(event) => {
                 let window = self.get_window(event.window)?;
-                let [atom, ..] = event.data.as_data32();
+                let [atom, _arg1, arg2, arg3, _arg4] = event.data.as_data32();
                 let mut state = self.0.borrow_mut();
 
                 if atom == state.atoms.WM_DELETE_WINDOW {
@@ -521,6 +521,12 @@ impl X11Client {
                         // Rest of the close logic is handled in drop_window()
                         window.close();
                     }
+                } else if atom == state.atoms._NET_WM_SYNC_REQUEST {
+                    window.state.borrow_mut().last_sync_counter =
+                        Some(x11rb::protocol::sync::Int64 {
+                            lo: arg2,
+                            hi: arg3 as i32,
+                        })
                 }
             }
             Event::ConfigureNotify(event) => {
@@ -537,6 +543,10 @@ impl X11Client {
                 let window = self.get_window(event.window)?;
                 window.configure(bounds);
             }
+            Event::PropertyNotify(event) => {
+                let window = self.get_window(event.window)?;
+                window.property_notify(event);
+            }
             Event::Expose(event) => {
                 let window = self.get_window(event.window)?;
                 window.refresh();

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

@@ -2,10 +2,11 @@ use anyhow::Context;
 
 use crate::{
     platform::blade::{BladeRenderer, BladeSurfaceConfig},
-    px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
-    PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
-    PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
-    WindowKind, WindowParams, X11ClientStatePtr,
+    px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, Modifiers,
+    Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
+    Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance,
+    WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams,
+    X11ClientStatePtr,
 };
 
 use blade_graphics as gpu;
@@ -15,24 +16,17 @@ use x11rb::{
     connection::Connection,
     protocol::{
         randr::{self, ConnectionExt as _},
+        sync,
         xinput::{self, ConnectionExt as _},
-        xproto::{
-            self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
-        },
+        xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply},
     },
     wrapper::ConnectionExt as _,
     xcb_ffi::XCBConnection,
 };
 
 use std::{
-    cell::RefCell,
-    ffi::c_void,
-    num::NonZeroU32,
-    ops::Div,
-    ptr::NonNull,
-    rc::Rc,
-    sync::{self, Arc},
-    time::Duration,
+    cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc,
+    sync::Arc, time::Duration,
 };
 
 use super::{X11Display, XINPUT_MASTER_DEVICE};
@@ -50,10 +44,16 @@ x11rb::atom_manager! {
         _NET_WM_STATE_HIDDEN,
         _NET_WM_STATE_FOCUSED,
         _NET_ACTIVE_WINDOW,
+        _NET_WM_SYNC_REQUEST,
+        _NET_WM_SYNC_REQUEST_COUNTER,
+        _NET_WM_BYPASS_COMPOSITOR,
         _NET_WM_MOVERESIZE,
         _NET_WM_WINDOW_TYPE,
         _NET_WM_WINDOW_TYPE_NOTIFICATION,
+        _NET_WM_SYNC,
+        _MOTIF_WM_HINTS,
         _GTK_SHOW_WINDOW_MENU,
+        _GTK_FRAME_EXTENTS,
     }
 }
 
@@ -70,6 +70,21 @@ fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window)
     }
 }
 
+impl ResizeEdge {
+    fn to_moveresize(&self) -> u32 {
+        match self {
+            ResizeEdge::TopLeft => 0,
+            ResizeEdge::Top => 1,
+            ResizeEdge::TopRight => 2,
+            ResizeEdge::Right => 3,
+            ResizeEdge::BottomRight => 4,
+            ResizeEdge::Bottom => 5,
+            ResizeEdge::BottomLeft => 6,
+            ResizeEdge::Left => 7,
+        }
+    }
+}
+
 #[derive(Debug)]
 struct Visual {
     id: xproto::Visualid,
@@ -166,6 +181,8 @@ pub struct X11WindowState {
     executor: ForegroundExecutor,
     atoms: XcbAtoms,
     x_root_window: xproto::Window,
+    pub(crate) counter_id: sync::Counter,
+    pub(crate) last_sync_counter: Option<sync::Int64>,
     _raw: RawWindow,
     bounds: Bounds<Pixels>,
     scale_factor: f32,
@@ -173,7 +190,22 @@ pub struct X11WindowState {
     display: Rc<dyn PlatformDisplay>,
     input_handler: Option<PlatformInputHandler>,
     appearance: WindowAppearance,
+    background_appearance: WindowBackgroundAppearance,
+    maximized_vertical: bool,
+    maximized_horizontal: bool,
+    hidden: bool,
+    active: bool,
+    fullscreen: bool,
+    decorations: WindowDecorations,
     pub handle: AnyWindowHandle,
+    last_insets: [u32; 4],
+}
+
+impl X11WindowState {
+    fn is_transparent(&self) -> bool {
+        self.decorations == WindowDecorations::Client
+            || self.background_appearance != WindowBackgroundAppearance::Opaque
+    }
 }
 
 #[derive(Clone)]
@@ -230,19 +262,11 @@ impl X11WindowState {
             .map_or(x_main_screen_index, |did| did.0 as usize);
 
         let visual_set = find_visuals(&xcb_connection, x_screen_index);
-        let visual_maybe = match params.window_background {
-            WindowBackgroundAppearance::Opaque => visual_set.opaque,
-            WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => {
-                visual_set.transparent
-            }
-        };
-        let visual = match visual_maybe {
+
+        let visual = match visual_set.transparent {
             Some(visual) => visual,
             None => {
-                log::warn!(
-                    "Unable to find a matching visual for {:?}",
-                    params.window_background
-                );
+                log::warn!("Unable to find a transparent visual",);
                 visual_set.inherit
             }
         };
@@ -269,7 +293,8 @@ impl X11WindowState {
                     | xproto::EventMask::STRUCTURE_NOTIFY
                     | xproto::EventMask::FOCUS_CHANGE
                     | xproto::EventMask::KEY_PRESS
-                    | xproto::EventMask::KEY_RELEASE,
+                    | xproto::EventMask::KEY_RELEASE
+                    | EventMask::PROPERTY_CHANGE,
             );
 
         let mut bounds = params.bounds.to_device_pixels(scale_factor);
@@ -349,7 +374,26 @@ impl X11WindowState {
                 x_window,
                 atoms.WM_PROTOCOLS,
                 xproto::AtomEnum::ATOM,
-                &[atoms.WM_DELETE_WINDOW],
+                &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST],
+            )
+            .unwrap();
+
+        sync::initialize(xcb_connection, 3, 1).unwrap();
+        let sync_request_counter = xcb_connection.generate_id().unwrap();
+        sync::create_counter(
+            xcb_connection,
+            sync_request_counter,
+            sync::Int64 { lo: 0, hi: 0 },
+        )
+        .unwrap();
+
+        xcb_connection
+            .change_property32(
+                xproto::PropMode::REPLACE,
+                x_window,
+                atoms._NET_WM_SYNC_REQUEST_COUNTER,
+                xproto::AtomEnum::CARDINAL,
+                &[sync_request_counter],
             )
             .unwrap();
 
@@ -396,7 +440,8 @@ impl X11WindowState {
             // Note: this has to be done after the GPU init, or otherwise
             // the sizes are immediately invalidated.
             size: query_render_extent(xcb_connection, x_window),
-            transparent: params.window_background != WindowBackgroundAppearance::Opaque,
+            // In case we have window decorations to render
+            transparent: true,
         };
         xcb_connection.map_window(x_window).unwrap();
 
@@ -438,9 +483,19 @@ impl X11WindowState {
             renderer: BladeRenderer::new(gpu, config),
             atoms: *atoms,
             input_handler: None,
+            active: false,
+            fullscreen: false,
+            maximized_vertical: false,
+            maximized_horizontal: false,
+            hidden: false,
             appearance,
             handle,
+            background_appearance: WindowBackgroundAppearance::Opaque,
             destroyed: false,
+            decorations: WindowDecorations::Server,
+            last_insets: [0, 0, 0, 0],
+            counter_id: sync_request_counter,
+            last_sync_counter: None,
             refresh_rate,
         })
     }
@@ -511,7 +566,7 @@ impl X11Window {
         scale_factor: f32,
         appearance: WindowAppearance,
     ) -> anyhow::Result<Self> {
-        Ok(Self(X11WindowStatePtr {
+        let ptr = X11WindowStatePtr {
             state: Rc::new(RefCell::new(X11WindowState::new(
                 handle,
                 client,
@@ -527,7 +582,12 @@ impl X11Window {
             callbacks: Rc::new(RefCell::new(Callbacks::default())),
             xcb_connection: xcb_connection.clone(),
             x_window,
-        }))
+        };
+
+        let state = ptr.state.borrow_mut();
+        ptr.set_wm_properties(state);
+
+        Ok(Self(ptr))
     }
 
     fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) {
@@ -549,29 +609,6 @@ impl X11Window {
             .unwrap();
     }
 
-    fn get_wm_hints(&self) -> Vec<u32> {
-        let reply = self
-            .0
-            .xcb_connection
-            .get_property(
-                false,
-                self.0.x_window,
-                self.0.state.borrow().atoms._NET_WM_STATE,
-                xproto::AtomEnum::ATOM,
-                0,
-                u32::MAX,
-            )
-            .unwrap()
-            .reply()
-            .unwrap();
-        // Reply is in u8 but atoms are represented as u32
-        reply
-            .value
-            .chunks_exact(4)
-            .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
-            .collect()
-    }
-
     fn get_root_position(&self, position: Point<Pixels>) -> TranslateCoordinatesReply {
         let state = self.0.state.borrow();
         self.0
@@ -586,6 +623,48 @@ impl X11Window {
             .reply()
             .unwrap()
     }
+
+    fn send_moveresize(&self, flag: u32) {
+        let state = self.0.state.borrow();
+
+        self.0
+            .xcb_connection
+            .ungrab_pointer(x11rb::CURRENT_TIME)
+            .unwrap()
+            .check()
+            .unwrap();
+
+        let pointer = self
+            .0
+            .xcb_connection
+            .query_pointer(self.0.x_window)
+            .unwrap()
+            .reply()
+            .unwrap();
+        let message = ClientMessageEvent::new(
+            32,
+            self.0.x_window,
+            state.atoms._NET_WM_MOVERESIZE,
+            [
+                pointer.root_x as u32,
+                pointer.root_y as u32,
+                flag,
+                0, // Left mouse button
+                0,
+            ],
+        );
+        self.0
+            .xcb_connection
+            .send_event(
+                false,
+                state.x_root_window,
+                EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
+                message,
+            )
+            .unwrap();
+
+        self.0.xcb_connection.flush().unwrap();
+    }
 }
 
 impl X11WindowStatePtr {
@@ -600,6 +679,54 @@ 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 {
+            self.set_wm_properties(state);
+        }
+    }
+
+    fn set_wm_properties(&self, mut state: std::cell::RefMut<X11WindowState>) {
+        let reply = self
+            .xcb_connection
+            .get_property(
+                false,
+                self.x_window,
+                state.atoms._NET_WM_STATE,
+                xproto::AtomEnum::ATOM,
+                0,
+                u32::MAX,
+            )
+            .unwrap()
+            .reply()
+            .unwrap();
+
+        let atoms = reply
+            .value
+            .chunks_exact(4)
+            .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
+
+        state.active = false;
+        state.fullscreen = false;
+        state.maximized_vertical = false;
+        state.maximized_horizontal = false;
+        state.hidden = true;
+
+        for atom in atoms {
+            if atom == state.atoms._NET_WM_STATE_FOCUSED {
+                state.active = true;
+            } else if atom == state.atoms._NET_WM_STATE_FULLSCREEN {
+                state.fullscreen = true;
+            } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT {
+                state.maximized_vertical = true;
+            } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ {
+                state.maximized_horizontal = true;
+            } else if atom == state.atoms._NET_WM_STATE_HIDDEN {
+                state.hidden = true;
+            }
+        }
+    }
+
     pub fn close(&self) {
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(fun) = callbacks.close.take() {
@@ -715,6 +842,9 @@ impl X11WindowStatePtr {
                 ));
                 resize_args = Some((state.content_size(), state.scale_factor));
             }
+            if let Some(value) = state.last_sync_counter.take() {
+                sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap();
+            }
         }
 
         let mut callbacks = self.callbacks.borrow_mut();
@@ -737,8 +867,12 @@ impl X11WindowStatePtr {
     }
 
     pub fn set_appearance(&mut self, appearance: WindowAppearance) {
-        self.state.borrow_mut().appearance = appearance;
-
+        let mut state = self.state.borrow_mut();
+        state.appearance = appearance;
+        let is_transparent = state.is_transparent();
+        state.renderer.update_transparency(is_transparent);
+        state.appearance = appearance;
+        drop(state);
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(ref mut fun) = callbacks.appearance_changed {
             (fun)()
@@ -757,11 +891,9 @@ impl PlatformWindow for X11Window {
 
     fn is_maximized(&self) -> bool {
         let state = self.0.state.borrow();
-        let wm_hints = self.get_wm_hints();
+
         // A maximized window that gets minimized will still retain its maximized state.
-        !wm_hints.contains(&state.atoms._NET_WM_STATE_HIDDEN)
-            && wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_VERT)
-            && wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_HORZ)
+        !state.hidden && state.maximized_vertical && state.maximized_horizontal
     }
 
     fn window_bounds(&self) -> WindowBounds {
@@ -862,9 +994,7 @@ impl PlatformWindow for X11Window {
     }
 
     fn is_active(&self) -> bool {
-        let state = self.0.state.borrow();
-        self.get_wm_hints()
-            .contains(&state.atoms._NET_WM_STATE_FOCUSED)
+        self.0.state.borrow().active
     }
 
     fn set_title(&mut self, title: &str) {
@@ -913,10 +1043,11 @@ impl PlatformWindow for X11Window {
         log::info!("ignoring macOS specific set_edited");
     }
 
-    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
-        let mut inner = self.0.state.borrow_mut();
-        let transparent = background_appearance != WindowBackgroundAppearance::Opaque;
-        inner.renderer.update_transparency(transparent);
+    fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
+        let mut state = self.0.state.borrow_mut();
+        state.background_appearance = background_appearance;
+        let transparent = state.is_transparent();
+        state.renderer.update_transparency(transparent);
     }
 
     fn show_character_palette(&self) {
@@ -962,9 +1093,7 @@ impl PlatformWindow for X11Window {
     }
 
     fn is_fullscreen(&self) -> bool {
-        let state = self.0.state.borrow();
-        self.get_wm_hints()
-            .contains(&state.atoms._NET_WM_STATE_FULLSCREEN)
+        self.0.state.borrow().fullscreen
     }
 
     fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
@@ -1004,7 +1133,7 @@ impl PlatformWindow for X11Window {
         inner.renderer.draw(scene);
     }
 
-    fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
+    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
         let inner = self.0.state.borrow();
         inner.renderer.sprite_atlas().clone()
     }
@@ -1035,41 +1164,109 @@ impl PlatformWindow for X11Window {
             .unwrap();
     }
 
-    fn start_system_move(&self) {
-        let state = self.0.state.borrow();
-        let pointer = self
-            .0
-            .xcb_connection
-            .query_pointer(self.0.x_window)
-            .unwrap()
-            .reply()
-            .unwrap();
+    fn start_window_move(&self) {
         const MOVERESIZE_MOVE: u32 = 8;
-        let message = ClientMessageEvent::new(
-            32,
-            self.0.x_window,
-            state.atoms._NET_WM_MOVERESIZE,
-            [
-                pointer.root_x as u32,
-                pointer.root_y as u32,
-                MOVERESIZE_MOVE,
-                1, // Left mouse button
-                1,
-            ],
-        );
+        self.send_moveresize(MOVERESIZE_MOVE);
+    }
+
+    fn start_window_resize(&self, edge: ResizeEdge) {
+        self.send_moveresize(edge.to_moveresize());
+    }
+
+    fn window_decorations(&self) -> crate::Decorations {
+        let state = self.0.state.borrow();
+
+        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 {
+                        top: state.maximized_vertical,
+                        bottom: state.maximized_vertical,
+                        left: state.maximized_horizontal,
+                        right: state.maximized_horizontal,
+                    },
+                }
+            }
+        }
+    }
+
+    fn set_client_inset(&self, inset: Pixels) {
+        let mut state = self.0.state.borrow_mut();
+
+        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)
+        } else {
+            (dp, dp)
+        };
+        let insets = [left, right, top, bottom];
+
+        if state.last_insets != insets {
+            state.last_insets = insets;
+
+            self.0
+                .xcb_connection
+                .change_property(
+                    xproto::PropMode::REPLACE,
+                    self.0.x_window,
+                    state.atoms._GTK_FRAME_EXTENTS,
+                    xproto::AtomEnum::CARDINAL,
+                    size_of::<u32>() as u8 * 8,
+                    4,
+                    bytemuck::cast_slice::<u32, u8>(&insets),
+                )
+                .unwrap();
+        }
+    }
+
+    fn request_decorations(&self, decorations: crate::WindowDecorations) {
+        // https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87
+        let hints_data: [u32; 5] = match decorations {
+            WindowDecorations::Server => [1 << 1, 0, 1, 0, 0],
+            WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
+        };
+
+        let mut state = self.0.state.borrow_mut();
+
         self.0
             .xcb_connection
-            .send_event(
-                false,
-                state.x_root_window,
-                EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
-                message,
+            .change_property(
+                xproto::PropMode::REPLACE,
+                self.0.x_window,
+                state.atoms._MOTIF_WM_HINTS,
+                state.atoms._MOTIF_WM_HINTS,
+                std::mem::size_of::<u32>() as u8 * 8,
+                5,
+                bytemuck::cast_slice::<u32, u8>(&hints_data),
             )
             .unwrap();
-    }
 
-    fn should_render_window_controls(&self) -> bool {
-        false
+        match decorations {
+            WindowDecorations::Server => {
+                state.decorations = WindowDecorations::Server;
+                let is_transparent = state.is_transparent();
+                state.renderer.update_transparency(is_transparent);
+            }
+            WindowDecorations::Client => {
+                state.decorations = WindowDecorations::Client;
+                let is_transparent = state.is_transparent();
+                state.renderer.update_transparency(is_transparent);
+            }
+        }
+
+        drop(state);
+        let mut callbacks = self.0.callbacks.borrow_mut();
+        if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
+            appearance_changed();
+        }
     }
 }
 

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

@@ -796,14 +796,24 @@ impl Platform for MacPlatform {
                 CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
                 CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
                 CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+                CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor],
                 CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
                 CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
-                CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
                 CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],
+                CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
                 CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
                 CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
-                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
-                CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
+
+                // Undocumented, private class methods:
+                // https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor
+                CursorStyle::ResizeUpLeftDownRight => {
+                    msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor]
+                }
+                CursorStyle::ResizeUpRightDownLeft => {
+                    msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor]
+                }
+
                 CursorStyle::IBeamCursorForVerticalLayout => {
                     msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
                 }

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

@@ -497,7 +497,6 @@ impl MacWindow {
     pub fn open(
         handle: AnyWindowHandle,
         WindowParams {
-            window_background,
             bounds,
             titlebar,
             kind,
@@ -603,7 +602,7 @@ impl MacWindow {
                     native_window as *mut _,
                     native_view as *mut _,
                     bounds.size.map(|pixels| pixels.0),
-                    window_background != WindowBackgroundAppearance::Opaque,
+                    false,
                 ),
                 request_frame_callback: None,
                 event_callback: None,
@@ -676,8 +675,6 @@ impl MacWindow {
             native_window.setContentView_(native_view.autorelease());
             native_window.makeFirstResponder_(native_view);
 
-            window.set_background_appearance(window_background);
-
             match kind {
                 WindowKind::Normal => {
                     native_window.setLevel_(NSNormalWindowLevel);
@@ -956,7 +953,7 @@ impl PlatformWindow for MacWindow {
 
     fn set_app_id(&mut self, _app_id: &str) {}
 
-    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
+    fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
         let mut this = self.0.as_ref().lock();
         this.renderer
             .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
@@ -1092,14 +1089,6 @@ impl PlatformWindow for MacWindow {
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
         self.0.lock().renderer.sprite_atlas().clone()
     }
-
-    fn show_window_menu(&self, _position: Point<Pixels>) {}
-
-    fn start_system_move(&self) {}
-
-    fn should_render_window_controls(&self) -> bool {
-        false
-    }
 }
 
 impl rwh::HasWindowHandle for MacWindow {

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

@@ -188,9 +188,7 @@ impl PlatformWindow for TestWindow {
 
     fn set_app_id(&mut self, _app_id: &str) {}
 
-    fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) {
-        unimplemented!()
-    }
+    fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
 
     fn set_edited(&mut self, edited: bool) {
         self.0.lock().edited = edited;
@@ -262,13 +260,9 @@ impl PlatformWindow for TestWindow {
         unimplemented!()
     }
 
-    fn start_system_move(&self) {
+    fn start_window_move(&self) {
         unimplemented!()
     }
-
-    fn should_render_window_controls(&self) -> bool {
-        false
-    }
 }
 
 pub(crate) struct TestAtlasState {

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

@@ -274,7 +274,7 @@ impl WindowsWindow {
             handle,
             hide_title_bar,
             display,
-            transparent: params.window_background != WindowBackgroundAppearance::Opaque,
+            transparent: true,
             executor,
             current_cursor,
         };
@@ -511,9 +511,7 @@ impl PlatformWindow for WindowsWindow {
             .ok();
     }
 
-    fn set_app_id(&mut self, _app_id: &str) {}
-
-    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
+    fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
         self.0
             .state
             .borrow_mut()
@@ -521,12 +519,6 @@ impl PlatformWindow for WindowsWindow {
             .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
     }
 
-    // todo(windows)
-    fn set_edited(&mut self, _edited: bool) {}
-
-    // todo(windows)
-    fn show_character_palette(&self) {}
-
     fn minimize(&self) {
         unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() };
     }
@@ -645,14 +637,6 @@ impl PlatformWindow for WindowsWindow {
     fn get_raw_handle(&self) -> HWND {
         self.0.hwnd
     }
-
-    fn show_window_menu(&self, _position: Point<Pixels>) {}
-
-    fn start_system_move(&self) {}
-
-    fn should_render_window_controls(&self) -> bool {
-        false
-    }
 }
 
 #[implement(IDropTarget)]

crates/gpui/src/window.rs 🔗

@@ -1,19 +1,20 @@
 use crate::{
     hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
     AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
-    Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
-    DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
-    FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding,
-    KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent,
-    LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent,
-    MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
-    PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
-    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
-    SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
-    TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
-    WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams,
-    WindowTextSystem, SUBPIXEL_VARIANTS,
+    Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
+    DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
+    FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData,
+    InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult,
+    Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
+    ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
+    Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
+    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString,
+    Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task,
+    TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View,
+    VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
+    WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
+    SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::{FxHashMap, FxHashSet};
@@ -610,7 +611,10 @@ fn default_bounds(display_id: Option<DisplayId>, cx: &mut AppContext) -> Bounds<
 
     cx.active_window()
         .and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok())
-        .map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET))
+        .map(|mut bounds| {
+            bounds.origin += DEFAULT_WINDOW_OFFSET;
+            bounds
+        })
         .unwrap_or_else(|| {
             let display = display_id
                 .map(|id| cx.find_display(id))
@@ -639,6 +643,7 @@ impl Window {
             window_background,
             app_id,
             window_min_size,
+            window_decorations,
         } = options;
 
         let bounds = window_bounds
@@ -654,7 +659,6 @@ impl Window {
                 focus,
                 show,
                 display_id,
-                window_background,
                 window_min_size,
             },
         )?;
@@ -672,6 +676,10 @@ impl Window {
         let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
         let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
 
+        platform_window
+            .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server));
+        platform_window.set_background_appearance(window_background);
+
         if let Some(ref window_open_state) = window_bounds {
             match window_open_state {
                 WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(),
@@ -990,6 +998,16 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.is_maximized()
     }
 
+    /// request a certain window decoration (Wayland)
+    pub fn request_decorations(&self, decorations: WindowDecorations) {
+        self.window.platform_window.request_decorations(decorations);
+    }
+
+    /// Start a window resize operation (Wayland)
+    pub fn start_window_resize(&self, edge: ResizeEdge) {
+        self.window.platform_window.start_window_resize(edge);
+    }
+
     /// Return the `WindowBounds` to indicate that how a window should be opened
     /// after it has been closed
     pub fn window_bounds(&self) -> WindowBounds {
@@ -1217,13 +1235,23 @@ impl<'a> WindowContext<'a> {
     /// Tells the compositor to take control of window movement (Wayland and X11)
     ///
     /// Events may not be received during a move operation.
-    pub fn start_system_move(&self) {
-        self.window.platform_window.start_system_move()
+    pub fn start_window_move(&self) {
+        self.window.platform_window.start_window_move()
+    }
+
+    /// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11)
+    pub fn set_client_inset(&self, inset: Pixels) {
+        self.window.platform_window.set_client_inset(inset);
     }
 
     /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11)
-    pub fn should_render_window_controls(&self) -> bool {
-        self.window.platform_window.should_render_window_controls()
+    pub fn window_decorations(&self) -> Decorations {
+        self.window.platform_window.window_decorations()
+    }
+
+    /// Returns which window controls are currently visible (Wayland)
+    pub fn window_controls(&self) -> WindowControls {
+        self.window.platform_window.window_controls()
     }
 
     /// Updates the window's title at the platform level.
@@ -1237,7 +1265,7 @@ impl<'a> WindowContext<'a> {
     }
 
     /// Sets the window background appearance.
-    pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
+    pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
         self.window
             .platform_window
             .set_background_appearance(background_appearance);

crates/theme/src/theme.rs 🔗

@@ -28,7 +28,8 @@ pub use settings::*;
 pub use styles::*;
 
 use gpui::{
-    AppContext, AssetSource, Hsla, SharedString, WindowAppearance, WindowBackgroundAppearance,
+    px, AppContext, AssetSource, Hsla, Pixels, SharedString, WindowAppearance,
+    WindowBackgroundAppearance,
 };
 use serde::Deserialize;
 
@@ -38,6 +39,9 @@ pub enum Appearance {
     Dark,
 }
 
+pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
+pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
+
 impl Appearance {
     pub fn is_light(&self) -> bool {
         match self {

crates/title_bar/src/platforms/platform_generic.rs 🔗

@@ -1,47 +0,0 @@
-use gpui::{prelude::*, Action};
-
-use ui::prelude::*;
-
-use crate::window_controls::{WindowControl, WindowControlType};
-
-#[derive(IntoElement)]
-pub struct GenericWindowControls {
-    close_window_action: Box<dyn Action>,
-}
-
-impl GenericWindowControls {
-    pub fn new(close_action: Box<dyn Action>) -> Self {
-        Self {
-            close_window_action: close_action,
-        }
-    }
-}
-
-impl RenderOnce for GenericWindowControls {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_flex()
-            .id("generic-window-controls")
-            .px_3()
-            .gap_1p5()
-            .child(WindowControl::new(
-                "minimize",
-                WindowControlType::Minimize,
-                cx,
-            ))
-            .child(WindowControl::new(
-                "maximize-or-restore",
-                if cx.is_maximized() {
-                    WindowControlType::Restore
-                } else {
-                    WindowControlType::Maximize
-                },
-                cx,
-            ))
-            .child(WindowControl::new_close(
-                "close",
-                WindowControlType::Close,
-                self.close_window_action,
-                cx,
-            ))
-    }
-}

crates/title_bar/src/platforms/platform_linux.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{prelude::*, Action};
 
 use ui::prelude::*;
 
-use super::platform_generic::GenericWindowControls;
+use crate::window_controls::{WindowControl, WindowControlType};
 
 #[derive(IntoElement)]
 pub struct LinuxWindowControls {
@@ -18,7 +18,30 @@ impl LinuxWindowControls {
 }
 
 impl RenderOnce for LinuxWindowControls {
-    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element()
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        h_flex()
+            .id("generic-window-controls")
+            .px_3()
+            .gap_3()
+            .child(WindowControl::new(
+                "minimize",
+                WindowControlType::Minimize,
+                cx,
+            ))
+            .child(WindowControl::new(
+                "maximize-or-restore",
+                if cx.is_maximized() {
+                    WindowControlType::Restore
+                } else {
+                    WindowControlType::Maximize
+                },
+                cx,
+            ))
+            .child(WindowControl::new_close(
+                "close",
+                WindowControlType::Close,
+                self.close_window_action,
+                cx,
+            ))
     }
 }

crates/title_bar/src/title_bar.rs 🔗

@@ -9,9 +9,9 @@ use call::{ActiveCall, ParticipantLocation};
 use client::{Client, UserStore};
 use collab::render_color_ribbon;
 use gpui::{
-    actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity,
-    IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled,
-    Subscription, ViewContext, VisualContext, WeakView,
+    actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
+    Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
+    StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
 };
 use project::{Project, RepositoryEntry};
 use recent_projects::RecentProjects;
@@ -58,6 +58,7 @@ pub struct TitleBar {
     user_store: Model<UserStore>,
     client: Arc<Client>,
     workspace: WeakView<Workspace>,
+    should_move: bool,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -73,8 +74,10 @@ impl Render for TitleBar {
         let platform_supported = cfg!(target_os = "macos");
 
         let height = Self::height(cx);
+        let supported_controls = cx.window_controls();
+        let decorations = cx.window_decorations();
 
-        let mut title_bar = h_flex()
+        h_flex()
             .id("titlebar")
             .w_full()
             .pt(Self::top_padding(cx))
@@ -88,6 +91,16 @@ impl Render for TitleBar {
                     this.pl_2()
                 }
             })
+            .map(|el| {
+                match decorations {
+                    Decorations::Server => el,
+                    Decorations::Client { tiling, .. } => el
+                        .when(!(tiling.top || tiling.right), |el| {
+                            el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                        })
+                        .when(!(tiling.top || tiling.left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING))
+                }
+            })
             .bg(cx.theme().colors().title_bar_background)
             .content_stretch()
             .child(
@@ -113,7 +126,7 @@ impl Render for TitleBar {
                                 .children(self.render_project_host(cx))
                                 .child(self.render_project_name(cx))
                                 .children(self.render_project_branch(cx))
-                                .on_mouse_move(|_, cx| cx.stop_propagation()),
+                                .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
                         )
                         .child(
                             h_flex()
@@ -145,7 +158,7 @@ impl Render for TitleBar {
 
                                         this.children(current_user_face_pile.map(|face_pile| {
                                             v_flex()
-                                                .on_mouse_move(|_, cx| cx.stop_propagation())
+                                                .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
                                                 .child(face_pile)
                                                 .child(render_color_ribbon(player_colors.local().cursor))
                                         }))
@@ -208,7 +221,7 @@ impl Render for TitleBar {
                             h_flex()
                                 .gap_1()
                                 .pr_1()
-                                .on_mouse_move(|_, cx| cx.stop_propagation())
+                                .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
                                 .when_some(room, |this, room| {
                                     let room = room.read(cx);
                                     let project = self.project.read(cx);
@@ -373,34 +386,38 @@ impl Render for TitleBar {
                                     }
                                 }),
                         )
-            );
 
-        // Windows Window Controls
-        title_bar = title_bar.when(
+            ).when(
             self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
             |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
-        );
-
-        // Linux Window Controls
-        title_bar = title_bar.when(
+        ).when(
             self.platform_style == PlatformStyle::Linux
                 && !cx.is_fullscreen()
-                && cx.should_render_window_controls(),
+                && matches!(decorations, Decorations::Client { .. }),
             |title_bar| {
                 title_bar
                     .child(platform_linux::LinuxWindowControls::new(close_action))
-                    .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
-                        cx.show_window_menu(ev.position)
-                    })
-                    .on_mouse_move(move |ev, cx| {
-                        if ev.dragging() {
-                            cx.start_system_move();
-                        }
+                    .when(supported_controls.window_menu, |titlebar| {
+                        titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
+                            cx.show_window_menu(ev.position)
+                        })
                     })
-            },
-        );
 
-        title_bar
+                                        .on_mouse_move(cx.listener(move |this, _ev, cx| {
+                                            if this.should_move {
+                                                this.should_move = false;
+                                                cx.start_window_move();
+                                            }
+                                        }))
+                                        .on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
+                                            this.should_move = false;
+                                        }))
+                                        .on_mouse_down(gpui::MouseButton::Left, cx.listener(move |this, _ev, _cx| {
+                                            this.should_move = true;
+                                    }))
+
+            },
+        )
     }
 }
 
@@ -430,6 +447,7 @@ impl TitleBar {
             content: div().id(id.into()),
             children: SmallVec::new(),
             workspace: workspace.weak_handle(),
+            should_move: false,
             project,
             user_store,
             client,

crates/title_bar/src/window_controls.rs 🔗

@@ -38,7 +38,7 @@ impl WindowControlStyle {
 
         Self {
             background: colors.ghost_element_background,
-            background_hover: colors.ghost_element_background,
+            background_hover: colors.ghost_element_hover,
             icon: colors.icon,
             icon_hover: colors.icon_muted,
         }
@@ -127,7 +127,7 @@ impl WindowControl {
 impl RenderOnce for WindowControl {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         let icon = svg()
-            .size_5()
+            .size_4()
             .flex_none()
             .path(self.icon.icon().path())
             .text_color(self.style.icon)
@@ -139,7 +139,7 @@ impl RenderOnce for WindowControl {
             .cursor_pointer()
             .justify_center()
             .content_center()
-            .rounded_md()
+            .rounded_2xl()
             .w_5()
             .h_5()
             .hover(|this| this.bg(self.style.background_hover))

crates/workspace/src/status_bar.rs 🔗

@@ -1,9 +1,10 @@
 use crate::{ItemHandle, Pane};
 use gpui::{
-    AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
-    WindowContext,
+    AnyView, Decorations, IntoElement, ParentElement, Render, Styled, Subscription, View,
+    ViewContext, WindowContext,
 };
 use std::any::TypeId;
+use theme::CLIENT_SIDE_DECORATION_ROUNDING;
 use ui::{h_flex, prelude::*};
 use util::ResultExt;
 
@@ -40,8 +41,17 @@ impl Render for StatusBar {
             .gap(Spacing::Large.rems(cx))
             .py(Spacing::Small.rems(cx))
             .px(Spacing::Large.rems(cx))
-            // .h_8()
             .bg(cx.theme().colors().status_bar_background)
+            .map(|el| match cx.window_decorations() {
+                Decorations::Server => el,
+                Decorations::Client { tiling, .. } => el
+                    .when(!(tiling.bottom || tiling.right), |el| {
+                        el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
+                    })
+                    .when(!(tiling.bottom || tiling.left), |el| {
+                        el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
+                    }),
+            })
             .child(self.render_left_tools(cx))
             .child(self.render_right_tools(cx))
     }

crates/workspace/src/workspace.rs 🔗

@@ -27,11 +27,13 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action,
-    AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds,
-    DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
-    KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel,
-    Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
+    action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
+    transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
+    AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
+    EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
+    Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
+    Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
+    WindowOptions,
 };
 use item::{
     FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@@ -4165,156 +4167,162 @@ impl Render for Workspace {
         let theme = cx.theme().clone();
         let colors = theme.colors();
 
-        self.actions(div(), cx)
-            .key_context(context)
-            .relative()
-            .size_full()
-            .flex()
-            .flex_col()
-            .font(ui_font)
-            .gap_0()
-            .justify_start()
-            .items_start()
-            .text_color(colors.text)
-            .bg(colors.background)
-            .children(self.titlebar_item.clone())
-            .child(
-                div()
-                    .id("workspace")
-                    .relative()
-                    .flex_1()
-                    .w_full()
-                    .flex()
-                    .flex_col()
-                    .overflow_hidden()
-                    .border_t_1()
-                    .border_b_1()
-                    .border_color(colors.border)
-                    .child({
-                        let this = cx.view().clone();
-                        canvas(
-                            move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
-                            |_, _, _| {},
-                        )
-                        .absolute()
-                        .size_full()
-                    })
-                    .when(self.zoomed.is_none(), |this| {
-                        this.on_drag_move(cx.listener(
-                            |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
-                                DockPosition::Left => {
-                                    let size = workspace.bounds.left() + e.event.position.x;
-                                    workspace.left_dock.update(cx, |left_dock, cx| {
-                                        left_dock.resize_active_panel(Some(size), cx);
-                                    });
-                                }
-                                DockPosition::Right => {
-                                    let size = workspace.bounds.right() - e.event.position.x;
-                                    workspace.right_dock.update(cx, |right_dock, cx| {
-                                        right_dock.resize_active_panel(Some(size), cx);
-                                    });
-                                }
-                                DockPosition::Bottom => {
-                                    let size = workspace.bounds.bottom() - e.event.position.y;
-                                    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
-                                        bottom_dock.resize_active_panel(Some(size), cx);
-                                    });
-                                }
-                            },
-                        ))
-                    })
-                    .child(
-                        div()
-                            .flex()
-                            .flex_row()
-                            .h_full()
-                            // Left Dock
-                            .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
-                                || {
-                                    div()
-                                        .flex()
-                                        .flex_none()
-                                        .overflow_hidden()
-                                        .child(self.left_dock.clone())
+        client_side_decorations(
+            self.actions(div(), cx)
+                .key_context(context)
+                .relative()
+                .size_full()
+                .flex()
+                .flex_col()
+                .font(ui_font)
+                .gap_0()
+                .justify_start()
+                .items_start()
+                .text_color(colors.text)
+                .overflow_hidden()
+                .children(self.titlebar_item.clone())
+                .child(
+                    div()
+                        .id("workspace")
+                        .bg(colors.background)
+                        .relative()
+                        .flex_1()
+                        .w_full()
+                        .flex()
+                        .flex_col()
+                        .overflow_hidden()
+                        .border_t_1()
+                        .border_b_1()
+                        .border_color(colors.border)
+                        .child({
+                            let this = cx.view().clone();
+                            canvas(
+                                move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
+                                |_, _, _| {},
+                            )
+                            .absolute()
+                            .size_full()
+                        })
+                        .when(self.zoomed.is_none(), |this| {
+                            this.on_drag_move(cx.listener(
+                                |workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
+                                    DockPosition::Left => {
+                                        let size = e.event.position.x - workspace.bounds.left();
+                                        workspace.left_dock.update(cx, |left_dock, cx| {
+                                            left_dock.resize_active_panel(Some(size), cx);
+                                        });
+                                    }
+                                    DockPosition::Right => {
+                                        let size = workspace.bounds.right() - e.event.position.x;
+                                        workspace.right_dock.update(cx, |right_dock, cx| {
+                                            right_dock.resize_active_panel(Some(size), cx);
+                                        });
+                                    }
+                                    DockPosition::Bottom => {
+                                        let size = workspace.bounds.bottom() - e.event.position.y;
+                                        workspace.bottom_dock.update(cx, |bottom_dock, cx| {
+                                            bottom_dock.resize_active_panel(Some(size), cx);
+                                        });
+                                    }
                                 },
                             ))
-                            // Panes
-                            .child(
-                                div()
-                                    .flex()
-                                    .flex_col()
-                                    .flex_1()
-                                    .overflow_hidden()
-                                    .child(
-                                        h_flex()
-                                            .flex_1()
-                                            .when_some(paddings.0, |this, p| {
-                                                this.child(p.border_r_1())
-                                            })
-                                            .child(self.center.render(
-                                                &self.project,
-                                                &self.follower_states,
-                                                self.active_call(),
-                                                &self.active_pane,
-                                                self.zoomed.as_ref(),
-                                                &self.app_state,
-                                                cx,
-                                            ))
-                                            .when_some(paddings.1, |this, p| {
-                                                this.child(p.border_l_1())
-                                            }),
-                                    )
-                                    .children(
-                                        self.zoomed_position
-                                            .ne(&Some(DockPosition::Bottom))
-                                            .then(|| self.bottom_dock.clone()),
-                                    ),
-                            )
-                            // Right Dock
-                            .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
-                                || {
+                        })
+                        .child(
+                            div()
+                                .flex()
+                                .flex_row()
+                                .h_full()
+                                // Left Dock
+                                .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
+                                    || {
+                                        div()
+                                            .flex()
+                                            .flex_none()
+                                            .overflow_hidden()
+                                            .child(self.left_dock.clone())
+                                    },
+                                ))
+                                // Panes
+                                .child(
                                     div()
                                         .flex()
-                                        .flex_none()
+                                        .flex_col()
+                                        .flex_1()
                                         .overflow_hidden()
-                                        .child(self.right_dock.clone())
-                                },
-                            )),
-                    )
-                    .children(self.zoomed.as_ref().and_then(|view| {
-                        let zoomed_view = view.upgrade()?;
-                        let div = div()
-                            .occlude()
-                            .absolute()
-                            .overflow_hidden()
-                            .border_color(colors.border)
-                            .bg(colors.background)
-                            .child(zoomed_view)
-                            .inset_0()
-                            .shadow_lg();
-
-                        Some(match self.zoomed_position {
-                            Some(DockPosition::Left) => div.right_2().border_r_1(),
-                            Some(DockPosition::Right) => div.left_2().border_l_1(),
-                            Some(DockPosition::Bottom) => div.top_2().border_t_1(),
-                            None => div.top_2().bottom_2().left_2().right_2().border_1(),
-                        })
-                    }))
-                    .child(self.modal_layer.clone())
-                    .children(self.render_notifications(cx)),
-            )
-            .child(self.status_bar.clone())
-            .children(if self.project.read(cx).is_disconnected() {
-                if let Some(render) = self.render_disconnected_overlay.take() {
-                    let result = render(self, cx);
-                    self.render_disconnected_overlay = Some(render);
-                    Some(result)
+                                        .child(
+                                            h_flex()
+                                                .flex_1()
+                                                .when_some(paddings.0, |this, p| {
+                                                    this.child(p.border_r_1())
+                                                })
+                                                .child(self.center.render(
+                                                    &self.project,
+                                                    &self.follower_states,
+                                                    self.active_call(),
+                                                    &self.active_pane,
+                                                    self.zoomed.as_ref(),
+                                                    &self.app_state,
+                                                    cx,
+                                                ))
+                                                .when_some(paddings.1, |this, p| {
+                                                    this.child(p.border_l_1())
+                                                }),
+                                        )
+                                        .children(
+                                            self.zoomed_position
+                                                .ne(&Some(DockPosition::Bottom))
+                                                .then(|| self.bottom_dock.clone()),
+                                        ),
+                                )
+                                // Right Dock
+                                .children(
+                                    self.zoomed_position.ne(&Some(DockPosition::Right)).then(
+                                        || {
+                                            div()
+                                                .flex()
+                                                .flex_none()
+                                                .overflow_hidden()
+                                                .child(self.right_dock.clone())
+                                        },
+                                    ),
+                                ),
+                        )
+                        .children(self.zoomed.as_ref().and_then(|view| {
+                            let zoomed_view = view.upgrade()?;
+                            let div = div()
+                                .occlude()
+                                .absolute()
+                                .overflow_hidden()
+                                .border_color(colors.border)
+                                .bg(colors.background)
+                                .child(zoomed_view)
+                                .inset_0()
+                                .shadow_lg();
+
+                            Some(match self.zoomed_position {
+                                Some(DockPosition::Left) => div.right_2().border_r_1(),
+                                Some(DockPosition::Right) => div.left_2().border_l_1(),
+                                Some(DockPosition::Bottom) => div.top_2().border_t_1(),
+                                None => div.top_2().bottom_2().left_2().right_2().border_1(),
+                            })
+                        }))
+                        .child(self.modal_layer.clone())
+                        .children(self.render_notifications(cx)),
+                )
+                .child(self.status_bar.clone())
+                .children(if self.project.read(cx).is_disconnected() {
+                    if let Some(render) = self.render_disconnected_overlay.take() {
+                        let result = render(self, cx);
+                        self.render_disconnected_overlay = Some(render);
+                        Some(result)
+                    } else {
+                        None
+                    }
                 } else {
                     None
-                }
-            } else {
-                None
-            })
+                }),
+            cx,
+        )
     }
 }
 
@@ -6474,3 +6482,267 @@ mod tests {
         });
     }
 }
+
+pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
+    const BORDER_SIZE: Pixels = px(1.0);
+    let decorations = cx.window_decorations();
+
+    if matches!(decorations, Decorations::Client { .. }) {
+        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
+    }
+
+    struct GlobalResizeEdge(ResizeEdge);
+    impl Global for GlobalResizeEdge {}
+
+    div()
+        .id("window-backdrop")
+        .bg(transparent_black())
+        .map(|div| match decorations {
+            Decorations::Server => div,
+            Decorations::Client { tiling, .. } => div
+                .child(
+                    canvas(
+                        |_bounds, cx| {
+                            cx.insert_hitbox(
+                                Bounds::new(
+                                    point(px(0.0), px(0.0)),
+                                    cx.window_bounds().get_bounds().size,
+                                ),
+                                false,
+                            )
+                        },
+                        move |_bounds, hitbox, cx| {
+                            let mouse = cx.mouse_position();
+                            let size = cx.window_bounds().get_bounds().size;
+                            let Some(edge) = resize_edge(
+                                mouse,
+                                theme::CLIENT_SIDE_DECORATION_SHADOW,
+                                size,
+                                tiling,
+                            ) else {
+                                return;
+                            };
+                            cx.set_global(GlobalResizeEdge(edge));
+                            cx.set_cursor_style(
+                                match edge {
+                                    ResizeEdge::Top | ResizeEdge::Bottom => {
+                                        CursorStyle::ResizeUpDown
+                                    }
+                                    ResizeEdge::Left | ResizeEdge::Right => {
+                                        CursorStyle::ResizeLeftRight
+                                    }
+                                    ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
+                                        CursorStyle::ResizeUpLeftDownRight
+                                    }
+                                    ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
+                                        CursorStyle::ResizeUpRightDownLeft
+                                    }
+                                },
+                                &hitbox,
+                            );
+                        },
+                    )
+                    .size_full()
+                    .absolute(),
+                )
+                .when(!(tiling.top || tiling.right), |div| {
+                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                })
+                .when(!(tiling.top || tiling.left), |div| {
+                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                })
+                .when(!(tiling.bottom || tiling.right), |div| {
+                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                })
+                .when(!(tiling.bottom || tiling.left), |div| {
+                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                })
+                .when(!tiling.top, |div| {
+                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
+                })
+                .when(!tiling.bottom, |div| {
+                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
+                })
+                .when(!tiling.left, |div| {
+                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
+                })
+                .when(!tiling.right, |div| {
+                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
+                })
+                .on_mouse_move(move |e, cx| {
+                    let size = cx.window_bounds().get_bounds().size;
+                    let pos = e.position;
+
+                    let new_edge =
+                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
+
+                    let edge = cx.try_global::<GlobalResizeEdge>();
+                    if new_edge != edge.map(|edge| edge.0) {
+                        cx.window_handle()
+                            .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
+                            .ok();
+                    }
+                })
+                .on_mouse_down(MouseButton::Left, move |e, cx| {
+                    let size = cx.window_bounds().get_bounds().size;
+                    let pos = e.position;
+
+                    let edge = match resize_edge(
+                        pos,
+                        theme::CLIENT_SIDE_DECORATION_SHADOW,
+                        size,
+                        tiling,
+                    ) {
+                        Some(value) => value,
+                        None => return,
+                    };
+
+                    cx.start_window_resize(edge);
+                }),
+        })
+        .size_full()
+        .child(
+            div()
+                .cursor(CursorStyle::Arrow)
+                .map(|div| match decorations {
+                    Decorations::Server => div,
+                    Decorations::Client { tiling } => div
+                        .border_color(cx.theme().colors().border)
+                        .when(!(tiling.top || tiling.right), |div| {
+                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                        })
+                        .when(!(tiling.top || tiling.left), |div| {
+                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                        })
+                        .when(!(tiling.bottom || tiling.right), |div| {
+                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                        })
+                        .when(!(tiling.bottom || tiling.left), |div| {
+                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+                        })
+                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
+                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
+                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
+                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
+                        .when(!tiling.is_tiled(), |div| {
+                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
+                                color: Hsla {
+                                    h: 0.,
+                                    s: 0.,
+                                    l: 0.,
+                                    a: 0.4,
+                                },
+                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
+                                spread_radius: px(0.),
+                                offset: point(px(0.0), px(0.0)),
+                            }])
+                        }),
+                })
+                .on_mouse_move(|_e, cx| {
+                    cx.stop_propagation();
+                })
+                .bg(cx.theme().colors().border)
+                .size_full()
+                .child(element),
+        )
+        .map(|div| match decorations {
+            Decorations::Server => div,
+            Decorations::Client { tiling, .. } => div.child(
+                canvas(
+                    |_bounds, cx| {
+                        cx.insert_hitbox(
+                            Bounds::new(
+                                point(px(0.0), px(0.0)),
+                                cx.window_bounds().get_bounds().size,
+                            ),
+                            false,
+                        )
+                    },
+                    move |_bounds, hitbox, cx| {
+                        let mouse = cx.mouse_position();
+                        let size = cx.window_bounds().get_bounds().size;
+                        let Some(edge) =
+                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
+                        else {
+                            return;
+                        };
+                        cx.set_global(GlobalResizeEdge(edge));
+                        cx.set_cursor_style(
+                            match edge {
+                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
+                                ResizeEdge::Left | ResizeEdge::Right => {
+                                    CursorStyle::ResizeLeftRight
+                                }
+                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
+                                    CursorStyle::ResizeUpLeftDownRight
+                                }
+                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
+                                    CursorStyle::ResizeUpRightDownLeft
+                                }
+                            },
+                            &hitbox,
+                        );
+                    },
+                )
+                .size_full()
+                .absolute(),
+            ),
+        })
+}
+
+fn resize_edge(
+    pos: Point<Pixels>,
+    shadow_size: Pixels,
+    window_size: Size<Pixels>,
+    tiling: Tiling,
+) -> Option<ResizeEdge> {
+    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
+    if bounds.contains(&pos) {
+        return None;
+    }
+
+    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
+    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
+    if top_left_bounds.contains(&pos) {
+        return Some(ResizeEdge::TopLeft);
+    }
+
+    let top_right_bounds = Bounds::new(
+        Point::new(window_size.width - corner_size.width, px(0.)),
+        corner_size,
+    );
+    if top_right_bounds.contains(&pos) {
+        return Some(ResizeEdge::TopRight);
+    }
+
+    let bottom_left_bounds = Bounds::new(
+        Point::new(px(0.), window_size.height - corner_size.height),
+        corner_size,
+    );
+    if bottom_left_bounds.contains(&pos) {
+        return Some(ResizeEdge::BottomLeft);
+    }
+
+    let bottom_right_bounds = Bounds::new(
+        Point::new(
+            window_size.width - corner_size.width,
+            window_size.height - corner_size.height,
+        ),
+        corner_size,
+    );
+    if bottom_right_bounds.contains(&pos) {
+        return Some(ResizeEdge::BottomRight);
+    }
+
+    if !tiling.top && pos.y < shadow_size {
+        Some(ResizeEdge::Top)
+    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
+        Some(ResizeEdge::Bottom)
+    } else if !tiling.left && pos.x < shadow_size {
+        Some(ResizeEdge::Left)
+    } else if !tiling.right && pos.x > window_size.width - shadow_size {
+        Some(ResizeEdge::Right)
+    } else {
+        None
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -105,6 +105,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
         display_id: display.map(|display| display.id()),
         window_background: cx.theme().window_background_appearance(),
         app_id: Some(app_id.to_owned()),
+        window_decorations: Some(gpui::WindowDecorations::Client),
         window_min_size: Some(gpui::Size {
             width: px(360.0),
             height: px(240.0),

crates/zed/src/zed/linux_prompts.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{
-    div, opaque_grey, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight,
-    InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
-    Render, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
+    div, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
+    IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Render,
+    RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
 };
 use settings::Settings;
 use theme::ThemeSettings;
@@ -101,35 +101,24 @@ impl Render for FallbackPromptRenderer {
                 }),
             ));
 
-        div()
-            .size_full()
-            .occlude()
-            .child(
-                div()
-                    .size_full()
-                    .bg(opaque_grey(0.5, 0.6))
-                    .absolute()
-                    .top_0()
-                    .left_0(),
-            )
-            .child(
-                div()
-                    .size_full()
-                    .absolute()
-                    .top_0()
-                    .left_0()
-                    .flex()
-                    .flex_col()
-                    .justify_around()
-                    .child(
-                        div()
-                            .w_full()
-                            .flex()
-                            .flex_row()
-                            .justify_around()
-                            .child(prompt),
-                    ),
-            )
+        div().size_full().occlude().child(
+            div()
+                .size_full()
+                .absolute()
+                .top_0()
+                .left_0()
+                .flex()
+                .flex_col()
+                .justify_around()
+                .child(
+                    div()
+                        .w_full()
+                        .flex()
+                        .flex_row()
+                        .justify_around()
+                        .child(prompt),
+                ),
+        )
     }
 }