gpui: Implement support for wlr layer shell (#35610)

Hilmar Wiegand and Ridan Vandenbergh created

This reintroduces `layer_shell` support after #32651 was reverted. On
top of that, it allows setting options for the created surface,
restricts the enum variant to the `wayland` feature, and adds an example
that renders a clock widget using the protocol.

I've renamed the `WindowKind` variant to `LayerShell` from `Overlay`,
since the protocol can also be used to render wallpapers and such, which
doesn't really fit with the word.

Things I'm still unsure of:
- We need to get the layer options types to the user somehow, but
nothing from the `platform::linux` crate was exported, I'm assuming
intentionally. I've kept the types inside the module (instead of doing
`pub use layer_shell::*` to not pollute the global namespace with
generic words like `Anchor` or `Layer` Let me know if you want to do
this differently.
- I've added the options to the `WindowKind` variant. That's the only
clean way I see to supply them when the window is created. This makes
the kind no longer implement `Copy`.
- The options don't have setter methods yet and can only be defined on
window creation. We'd have to make fallible functions for setting them,
which only work if the underlying surface is a `layer_shell` surface.
That feels un-rust-y.

CC @zeroeightysix  
Thanks to @wuliuqii, whose layer-shell implementation I've also looked
at while putting this together.

Release Notes:

- Add support for the `layer_shell` protocol on wayland

---------

Co-authored-by: Ridan Vandenbergh <ridanvandenbergh@gmail.com>

Change summary

Cargo.lock                                            |  15 
crates/gpui/Cargo.toml                                |   6 
crates/gpui/examples/layer_shell.rs                   |  87 +++
crates/gpui/src/platform.rs                           |  28 
crates/gpui/src/platform/linux.rs                     |   3 
crates/gpui/src/platform/linux/wayland.rs             |   3 
crates/gpui/src/platform/linux/wayland/client.rs      |  34 +
crates/gpui/src/platform/linux/wayland/layer_shell.rs | 111 ++++
crates/gpui/src/platform/linux/wayland/window.rs      | 336 ++++++++++--
9 files changed, 541 insertions(+), 82 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7263,6 +7263,7 @@ dependencies = [
  "async-task",
  "backtrace",
  "bindgen 0.71.1",
+ "bitflags 2.9.4",
  "blade-graphics",
  "blade-macros",
  "blade-util",
@@ -7342,6 +7343,7 @@ dependencies = [
  "wayland-cursor",
  "wayland-protocols 0.31.2",
  "wayland-protocols-plasma",
+ "wayland-protocols-wlr",
  "windows 0.61.3",
  "windows-core 0.61.2",
  "windows-numerics",
@@ -19490,6 +19492,19 @@ dependencies = [
  "wayland-scanner",
 ]
 
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
+dependencies = [
+ "bitflags 2.9.4",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols 0.32.9",
+ "wayland-scanner",
+]
+
 [[package]]
 name = "wayland-scanner"
 version = "0.31.7"

crates/gpui/Cargo.toml 🔗

@@ -39,6 +39,7 @@ macos-blade = [
     "objc2-metal",
 ]
 wayland = [
+    "bitflags",
     "blade-graphics",
     "blade-macros",
     "blade-util",
@@ -52,6 +53,7 @@ wayland = [
     "wayland-cursor",
     "wayland-protocols",
     "wayland-protocols-plasma",
+    "wayland-protocols-wlr",
     "filedescriptor",
     "xkbcommon",
     "open",
@@ -86,6 +88,7 @@ doctest = false
 anyhow.workspace = true
 async-task = "4.7"
 backtrace = { workspace = true, optional = true }
+bitflags = { workspace = true, optional = true }
 blade-graphics = { workspace = true, optional = true }
 blade-macros = { workspace = true, optional = true }
 blade-util = { workspace = true, optional = true }
@@ -202,6 +205,9 @@ wayland-protocols = { version = "0.31.2", features = [
 wayland-protocols-plasma = { version = "0.2.0", features = [
     "client",
 ], optional = true }
+wayland-protocols-wlr = { version = "0.3.9", features = [
+    "client",
+], optional = true }
 
 # X11
 as-raw-xcb-connection = { version = "1", optional = true }

crates/gpui/examples/layer_shell.rs 🔗

@@ -0,0 +1,87 @@
+fn main() {
+    #[cfg(all(target_os = "linux", feature = "wayland"))]
+    example::main();
+
+    #[cfg(not(all(target_os = "linux", feature = "wayland")))]
+    panic!("This example requires the `wayland` feature and a linux system.");
+}
+
+#[cfg(all(target_os = "linux", feature = "wayland"))]
+mod example {
+    use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+    use gpui::{
+        App, Application, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance,
+        WindowBounds, WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems,
+        rgba, white,
+    };
+
+    struct LayerShellExample;
+
+    impl LayerShellExample {
+        fn new(cx: &mut Context<Self>) -> Self {
+            cx.spawn(async move |this, cx| {
+                loop {
+                    let _ = this.update(cx, |_, cx| cx.notify());
+                    cx.background_executor()
+                        .timer(Duration::from_millis(500))
+                        .await;
+                }
+            })
+            .detach();
+
+            LayerShellExample
+        }
+    }
+
+    impl Render for LayerShellExample {
+        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+            let now = SystemTime::now()
+                .duration_since(UNIX_EPOCH)
+                .unwrap()
+                .as_secs();
+
+            let hours = (now / 3600) % 24;
+            let minutes = (now / 60) % 60;
+            let seconds = now % 60;
+
+            div()
+                .size_full()
+                .flex()
+                .items_center()
+                .justify_center()
+                .text_size(rems(4.5))
+                .font_weight(FontWeight::EXTRA_BOLD)
+                .text_color(white())
+                .bg(rgba(0x0000044))
+                .rounded_xl()
+                .child(format!("{:02}:{:02}:{:02}", hours, minutes, seconds))
+        }
+    }
+
+    pub fn main() {
+        Application::new().run(|cx: &mut App| {
+            cx.open_window(
+                WindowOptions {
+                    titlebar: None,
+                    window_bounds: Some(WindowBounds::Windowed(Bounds {
+                        origin: point(px(0.), px(0.)),
+                        size: Size::new(px(500.), px(200.)),
+                    })),
+                    app_id: Some("gpui-layer-shell-example".to_string()),
+                    window_background: WindowBackgroundAppearance::Transparent,
+                    kind: WindowKind::LayerShell(LayerShellOptions {
+                        namespace: "gpui".to_string(),
+                        anchor: Anchor::LEFT | Anchor::RIGHT | Anchor::BOTTOM,
+                        margin: Some((px(0.), px(0.), px(40.), px(0.))),
+                        keyboard_interactivity: KeyboardInteractivity::None,
+                        ..Default::default()
+                    }),
+                    ..Default::default()
+                },
+                |_, cx| cx.new(LayerShellExample::new),
+            )
+            .unwrap();
+        });
+    }
+}

crates/gpui/src/platform.rs 🔗

@@ -82,6 +82,9 @@ pub(crate) use test::*;
 #[cfg(target_os = "windows")]
 pub(crate) use windows::*;
 
+#[cfg(all(target_os = "linux", feature = "wayland"))]
+pub use linux::layer_shell;
+
 #[cfg(any(test, feature = "test-support"))]
 pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
 
@@ -120,6 +123,15 @@ pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
     }
 }
 
+#[cfg(target_os = "windows")]
+pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
+    Rc::new(
+        WindowsPlatform::new()
+            .inspect_err(|err| show_error("Failed to launch", err.to_string()))
+            .unwrap(),
+    )
+}
+
 /// Return which compositor we're guessing we'll use.
 /// Does not attempt to connect to the given compositor
 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -151,15 +163,6 @@ pub fn guess_compositor() -> &'static str {
     }
 }
 
-#[cfg(target_os = "windows")]
-pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
-    Rc::new(
-        WindowsPlatform::new()
-            .inspect_err(|err| show_error("Failed to launch", err.to_string()))
-            .unwrap(),
-    )
-}
-
 pub(crate) trait Platform: 'static {
     fn background_executor(&self) -> BackgroundExecutor;
     fn foreground_executor(&self) -> ForegroundExecutor;
@@ -1293,7 +1296,7 @@ pub struct TitlebarOptions {
 }
 
 /// The kind of window to create
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum WindowKind {
     /// A normal application window
     Normal,
@@ -1304,6 +1307,11 @@ pub enum WindowKind {
 
     /// A floating window that appears on top of its parent window
     Floating,
+
+    /// A Wayland LayerShell window, used to draw overlays or backgrounds for applications such as
+    /// docks, notifications or wallpapers.
+    #[cfg(all(target_os = "linux", feature = "wayland"))]
+    LayerShell(layer_shell::LayerShellOptions),
 }
 
 /// The appearance of the window, as defined by the operating system.

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

@@ -27,3 +27,6 @@ pub(crate) use x11::*;
 pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
 #[cfg(not(all(feature = "screen-capture", any(feature = "wayland", feature = "x11"))))]
 pub(crate) type PlatformScreenCaptureFrame = ();
+
+#[cfg(feature = "wayland")]
+pub use wayland::layer_shell;

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

@@ -5,6 +5,9 @@ mod display;
 mod serial;
 mod window;
 
+/// Contains Types for configuring layer_shell surfaces.
+pub mod layer_shell;
+
 pub(crate) use client::*;
 
 use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;

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

@@ -62,6 +62,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
 };
 use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
 use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
+use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
 use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
 use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
 
@@ -115,6 +116,7 @@ pub struct Globals {
     pub fractional_scale_manager:
         Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
     pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
+    pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
     pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
     pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
     pub executor: ForegroundExecutor,
@@ -152,6 +154,7 @@ impl Globals {
             viewporter: globals.bind(&qh, 1..=1, ()).ok(),
             fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
             decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
+            layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
             blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
             text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
             executor,
@@ -695,7 +698,10 @@ impl LinuxClient for WaylandClient {
     ) -> anyhow::Result<Box<dyn PlatformWindow>> {
         let mut state = self.0.borrow_mut();
 
-        let parent = state.keyboard_focused_window.as_ref().map(|w| w.toplevel());
+        let parent = state
+            .keyboard_focused_window
+            .as_ref()
+            .and_then(|w| w.toplevel());
 
         let (window, surface_id) = WaylandWindow::new(
             handle,
@@ -945,6 +951,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
 delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion);
 delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
 delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
+delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
 delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
 delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3);
 delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
@@ -1087,6 +1094,31 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
     }
 }
 
+impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ObjectId> for WaylandClientStatePtr {
+    fn event(
+        this: &mut Self,
+        _: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
+        event: <zwlr_layer_surface_v1::ZwlrLayerSurfaceV1 as Proxy>::Event,
+        surface_id: &ObjectId,
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+        let client = this.get_client();
+        let mut state = client.borrow_mut();
+        let Some(window) = get_window(&mut state, surface_id) else {
+            return;
+        };
+
+        drop(state);
+        let should_close = window.handle_layersurface_event(event);
+
+        if should_close {
+            // The close logic will be handled in drop_window()
+            window.close();
+        }
+    }
+}
+
 impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientStatePtr {
     fn event(
         _: &mut Self,

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

@@ -0,0 +1,111 @@
+use bitflags::bitflags;
+use thiserror::Error;
+use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
+
+use crate::Pixels;
+
+/// The layer the surface is rendered on. Multiple surfaces can share a layer, and ordering within
+/// a single layer is undefined.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum Layer {
+    /// The background layer, typically used for wallpapers.
+    Background,
+
+    /// The bottom layer.
+    Bottom,
+
+    /// The top layer, typically used for fullscreen windows.
+    Top,
+
+    /// The overlay layer, used for surfaces that should always be on top.
+    #[default]
+    Overlay,
+}
+
+impl From<Layer> for zwlr_layer_shell_v1::Layer {
+    fn from(layer: Layer) -> Self {
+        match layer {
+            Layer::Background => Self::Background,
+            Layer::Bottom => Self::Bottom,
+            Layer::Top => Self::Top,
+            Layer::Overlay => Self::Overlay,
+        }
+    }
+}
+
+bitflags! {
+    /// Screen anchor point for layer_shell surfaces. These can be used in any combination, e.g.
+    /// specifying `Anchor::LEFT | Anchor::RIGHT` will stretch the surface across the width of the
+    /// screen.
+    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+    pub struct Anchor: u32 {
+        /// Anchor to the top edge of the screen.
+        const TOP = 1;
+        /// Anchor to the bottom edge of the screen.
+        const BOTTOM = 2;
+        /// Anchor to the left edge of the screen.
+        const LEFT = 4;
+        /// Anchor to the right edge of the screen.
+        const RIGHT = 8;
+    }
+}
+
+impl From<Anchor> for zwlr_layer_surface_v1::Anchor {
+    fn from(anchor: Anchor) -> Self {
+        Self::from_bits_truncate(anchor.bits())
+    }
+}
+
+/// Keyboard interactivity mode for the layer_shell surfaces.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum KeyboardInteractivity {
+    /// No keyboard inputs will be delivered to the surface and it won't be able to receive
+    /// keyboard focus.
+    None,
+
+    /// The surface will receive exclusive keyboard focus as long as it is above the shell surface
+    /// layer, and no other layer_shell surfaces are above it.
+    Exclusive,
+
+    /// The surface can be focused similarly to a normal window.
+    #[default]
+    OnDemand,
+}
+
+impl From<KeyboardInteractivity> for zwlr_layer_surface_v1::KeyboardInteractivity {
+    fn from(value: KeyboardInteractivity) -> Self {
+        match value {
+            KeyboardInteractivity::None => Self::None,
+            KeyboardInteractivity::Exclusive => Self::Exclusive,
+            KeyboardInteractivity::OnDemand => Self::OnDemand,
+        }
+    }
+}
+
+/// Options for creating a layer_shell window.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct LayerShellOptions {
+    /// The namespace for the surface, mostly used by compositors to apply rules, can not be
+    /// changed after the surface is created.
+    pub namespace: String,
+    /// The layer the surface is rendered on.
+    pub layer: Layer,
+    /// The anchor point of the surface.
+    pub anchor: Anchor,
+    /// Requests that the compositor avoids occluding an area with other surfaces.
+    pub exclusive_zone: Option<Pixels>,
+    /// The anchor point of the exclusive zone, will be determined using the anchor if left
+    /// unspecified.
+    pub exclusive_edge: Option<Anchor>,
+    /// Margins between the surface and its anchor point(s).
+    /// Specified in CSS order: top, right, bottom, left.
+    pub margin: Option<(Pixels, Pixels, Pixels, Pixels)>,
+    /// How keyboard events should be delivered to the surface.
+    pub keyboard_interactivity: KeyboardInteractivity,
+}
+
+/// An error indicating that an action failed because the compositor doesn't support the required
+/// layer_shell protocol.
+#[derive(Debug, Error)]
+#[error("Compositor doesn't support zwlr_layer_shell_v1")]
+pub struct LayerShellNotSupportedError;

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

@@ -23,12 +23,14 @@ use wayland_protocols::{
     xdg::shell::client::xdg_toplevel::XdgToplevel,
 };
 use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
+use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
 
 use crate::{
     AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
     PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
     ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
-    WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, size,
+    WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
+    layer_shell::LayerShellNotSupportedError, px, size,
 };
 use crate::{
     Capslock,
@@ -83,14 +85,12 @@ struct InProgressConfigure {
 }
 
 pub struct WaylandWindowState {
-    xdg_surface: xdg_surface::XdgSurface,
+    surface_state: WaylandSurfaceState,
     acknowledged_first_configure: bool,
     pub surface: wl_surface::WlSurface,
-    decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
     app_id: Option<String>,
     appearance: WindowAppearance,
     blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
-    toplevel: xdg_toplevel::XdgToplevel,
     viewport: Option<wp_viewport::WpViewport>,
     outputs: HashMap<ObjectId, Output>,
     display: Option<(ObjectId, Output)>,
@@ -116,6 +116,161 @@ pub struct WaylandWindowState {
     client_inset: Option<Pixels>,
 }
 
+pub enum WaylandSurfaceState {
+    Xdg(WaylandXdgSurfaceState),
+    LayerShell(WaylandLayerSurfaceState),
+}
+
+impl WaylandSurfaceState {
+    fn new(
+        surface: &wl_surface::WlSurface,
+        globals: &Globals,
+        params: &WindowParams,
+        parent: Option<XdgToplevel>,
+    ) -> anyhow::Result<Self> {
+        // For layer_shell windows, create a layer surface instead of an xdg surface
+        if let WindowKind::LayerShell(options) = &params.kind {
+            let Some(layer_shell) = globals.layer_shell.as_ref() else {
+                return Err(LayerShellNotSupportedError.into());
+            };
+
+            let layer_surface = layer_shell.get_layer_surface(
+                &surface,
+                None,
+                options.layer.into(),
+                options.namespace.clone(),
+                &globals.qh,
+                surface.id(),
+            );
+
+            let width = params.bounds.size.width.0;
+            let height = params.bounds.size.height.0;
+            layer_surface.set_size(width as u32, height as u32);
+
+            layer_surface.set_anchor(options.anchor.into());
+            layer_surface.set_keyboard_interactivity(options.keyboard_interactivity.into());
+
+            if let Some(margin) = options.margin {
+                layer_surface.set_margin(
+                    margin.0.0 as i32,
+                    margin.1.0 as i32,
+                    margin.2.0 as i32,
+                    margin.3.0 as i32,
+                )
+            }
+
+            if let Some(exclusive_zone) = options.exclusive_zone {
+                layer_surface.set_exclusive_zone(exclusive_zone.0 as i32);
+            }
+
+            if let Some(exclusive_edge) = options.exclusive_edge {
+                layer_surface.set_exclusive_edge(exclusive_edge.into());
+            }
+
+            return Ok(WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState {
+                layer_surface,
+            }));
+        }
+
+        // All other WindowKinds result in a regular xdg surface
+        let xdg_surface = globals
+            .wm_base
+            .get_xdg_surface(&surface, &globals.qh, surface.id());
+
+        let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
+        if params.kind == WindowKind::Floating {
+            toplevel.set_parent(parent.as_ref());
+        }
+
+        if let Some(size) = params.window_min_size {
+            toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
+        }
+
+        // Attempt to set up window decorations based on the requested configuration
+        let decoration = globals
+            .decoration_manager
+            .as_ref()
+            .map(|decoration_manager| {
+                decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
+            });
+
+        Ok(WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
+            xdg_surface,
+            toplevel,
+            decoration,
+        }))
+    }
+}
+
+pub struct WaylandXdgSurfaceState {
+    xdg_surface: xdg_surface::XdgSurface,
+    toplevel: xdg_toplevel::XdgToplevel,
+    decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
+}
+
+pub struct WaylandLayerSurfaceState {
+    layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
+}
+
+impl WaylandSurfaceState {
+    fn ack_configure(&self, serial: u32) {
+        match self {
+            WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => {
+                xdg_surface.ack_configure(serial);
+            }
+            WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
+                layer_surface.ack_configure(serial);
+            }
+        }
+    }
+
+    fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> {
+        if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self {
+            decoration.as_ref()
+        } else {
+            None
+        }
+    }
+
+    fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> {
+        if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self {
+            Some(toplevel)
+        } else {
+            None
+        }
+    }
+
+    fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) {
+        match self {
+            WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => {
+                xdg_surface.set_window_geometry(x, y, width, height);
+            }
+            WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
+                // cannot set window position of a layer surface
+                layer_surface.set_size(width as u32, height as u32);
+            }
+        }
+    }
+
+    fn destroy(&mut self) {
+        match self {
+            WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
+                xdg_surface,
+                toplevel,
+                decoration: _decoration,
+            }) => {
+                // The role object (toplevel) must always be destroyed before the xdg_surface.
+                // See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
+                toplevel.destroy();
+                xdg_surface.destroy();
+            }
+            WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => {
+                layer_surface.destroy();
+            }
+        }
+    }
+}
+
 #[derive(Clone)]
 pub struct WaylandWindowStatePtr {
     state: Rc<RefCell<WaylandWindowState>>,
@@ -126,9 +281,7 @@ impl WaylandWindowState {
     pub(crate) fn new(
         handle: AnyWindowHandle,
         surface: wl_surface::WlSurface,
-        xdg_surface: xdg_surface::XdgSurface,
-        toplevel: xdg_toplevel::XdgToplevel,
-        decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
+        surface_state: WaylandSurfaceState,
         appearance: WindowAppearance,
         viewport: Option<wp_viewport::WpViewport>,
         client: WaylandClientStatePtr,
@@ -157,20 +310,18 @@ impl WaylandWindowState {
             BladeRenderer::new(gpu_context, &raw_window, config)?
         };
 
-        if let Some(titlebar) = options.titlebar {
-            if let Some(title) = titlebar.title {
-                toplevel.set_title(title.to_string());
+        if let WaylandSurfaceState::Xdg(ref xdg_state) = surface_state {
+            if let Some(title) = options.titlebar.and_then(|titlebar| titlebar.title) {
+                xdg_state.toplevel.set_title(title.to_string());
             }
         }
 
         Ok(Self {
-            xdg_surface,
+            surface_state,
             acknowledged_first_configure: false,
             surface,
-            decoration,
             app_id: None,
             blur: None,
-            toplevel,
             viewport,
             globals,
             outputs: HashMap::default(),
@@ -243,17 +394,29 @@ impl Drop for WaylandWindow {
         let client = state.client.clone();
 
         state.renderer.destroy();
-        if let Some(decoration) = &state.decoration {
-            decoration.destroy();
-        }
+
+        // Destroy blur first, this has no dependencies.
         if let Some(blur) = &state.blur {
             blur.release();
         }
-        state.toplevel.destroy();
+
+        // Decorations must be destroyed before the xdg state.
+        // See https://wayland.app/protocols/xdg-decoration-unstable-v1#zxdg_toplevel_decoration_v1
+        if let Some(decoration) = &state.surface_state.decoration() {
+            decoration.destroy();
+        }
+
+        // Surface state might contain xdg_toplevel/xdg_surface which can be destroyed now that
+        // decorations are gone. layer_surface has no dependencies.
+        state.surface_state.destroy();
+
+        // Viewport must be destroyed before the wl_surface.
+        // See https://wayland.app/protocols/viewporter#wp_viewport
         if let Some(viewport) = &state.viewport {
             viewport.destroy();
         }
-        state.xdg_surface.destroy();
+
+        // The wl_surface itself should always be destroyed last.
         state.surface.destroy();
 
         let state_ptr = self.0.clone();
@@ -288,31 +451,12 @@ impl WaylandWindow {
         parent: Option<XdgToplevel>,
     ) -> anyhow::Result<(Self, ObjectId)> {
         let surface = globals.compositor.create_surface(&globals.qh, ());
-        let xdg_surface = globals
-            .wm_base
-            .get_xdg_surface(&surface, &globals.qh, surface.id());
-        let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
-
-        if params.kind == WindowKind::Floating {
-            toplevel.set_parent(parent.as_ref());
-        }
-
-        if let Some(size) = params.window_min_size {
-            toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
-        }
+        let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent)?;
 
         if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
             fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
         }
 
-        // Attempt to set up window decorations based on the requested configuration
-        let decoration = globals
-            .decoration_manager
-            .as_ref()
-            .map(|decoration_manager| {
-                decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
-            });
-
         let viewport = globals
             .viewporter
             .as_ref()
@@ -322,9 +466,7 @@ impl WaylandWindow {
             state: Rc::new(RefCell::new(WaylandWindowState::new(
                 handle,
                 surface.clone(),
-                xdg_surface,
-                toplevel,
-                decoration,
+                surface_state,
                 appearance,
                 viewport,
                 client,
@@ -351,8 +493,8 @@ impl WaylandWindowStatePtr {
         self.state.borrow().surface.clone()
     }
 
-    pub fn toplevel(&self) -> xdg_toplevel::XdgToplevel {
-        self.state.borrow().toplevel.clone()
+    pub fn toplevel(&self) -> Option<xdg_toplevel::XdgToplevel> {
+        self.state.borrow().surface_state.toplevel().cloned()
     }
 
     pub fn ptr_eq(&self, other: &Self) -> bool {
@@ -419,7 +561,7 @@ impl WaylandWindowStatePtr {
                 }
             }
             let mut state = self.state.borrow_mut();
-            state.xdg_surface.ack_configure(serial);
+            state.surface_state.ack_configure(serial);
 
             let window_geometry = inset_by_tiling(
                 state.bounds.map_origin(|_| px(0.0)),
@@ -429,7 +571,7 @@ impl WaylandWindowStatePtr {
             .map(|v| v.0 as i32)
             .map_size(|v| if v <= 0 { 1 } else { v });
 
-            state.xdg_surface.set_window_geometry(
+            state.surface_state.set_geometry(
                 window_geometry.origin.x,
                 window_geometry.origin.y,
                 window_geometry.size.width,
@@ -588,6 +730,42 @@ impl WaylandWindowStatePtr {
         }
     }
 
+    pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool {
+        match event {
+            zwlr_layer_surface_v1::Event::Configure {
+                width,
+                height,
+                serial,
+            } => {
+                let mut size = if width == 0 || height == 0 {
+                    None
+                } else {
+                    Some(size(px(width as f32), px(height as f32)))
+                };
+
+                let mut state = self.state.borrow_mut();
+                state.in_progress_configure = Some(InProgressConfigure {
+                    size,
+                    fullscreen: false,
+                    maximized: false,
+                    resizing: false,
+                    tiling: Tiling::default(),
+                });
+                drop(state);
+
+                // just do the same thing we'd do as an xdg_surface
+                self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial });
+
+                false
+            }
+            zwlr_layer_surface_v1::Event::Closed => {
+                // unlike xdg, we don't have a choice here: the surface is closing.
+                true
+            }
+            _ => false,
+        }
+    }
+
     #[allow(clippy::mutable_key_type)]
     pub fn handle_surface_event(
         &self,
@@ -849,7 +1027,7 @@ impl PlatformWindow for WaylandWindow {
         let state_ptr = self.0.clone();
         let dp_size = size.to_device_pixels(self.scale_factor());
 
-        state.xdg_surface.set_window_geometry(
+        state.surface_state.set_geometry(
             state.bounds.origin.x.0 as i32,
             state.bounds.origin.y.0 as i32,
             dp_size.width.0,
@@ -943,12 +1121,16 @@ impl PlatformWindow for WaylandWindow {
     }
 
     fn set_title(&mut self, title: &str) {
-        self.borrow().toplevel.set_title(title.to_string());
+        if let Some(toplevel) = self.borrow().surface_state.toplevel() {
+            toplevel.set_title(title.to_string());
+        }
     }
 
     fn set_app_id(&mut self, app_id: &str) {
         let mut state = self.borrow_mut();
-        state.toplevel.set_app_id(app_id.to_owned());
+        if let Some(toplevel) = state.surface_state.toplevel() {
+            toplevel.set_app_id(app_id.to_owned());
+        }
         state.app_id = Some(app_id.to_owned());
     }
 
@@ -959,24 +1141,30 @@ impl PlatformWindow for WaylandWindow {
     }
 
     fn minimize(&self) {
-        self.borrow().toplevel.set_minimized();
+        if let Some(toplevel) = self.borrow().surface_state.toplevel() {
+            toplevel.set_minimized();
+        }
     }
 
     fn zoom(&self) {
         let state = self.borrow();
-        if !state.maximized {
-            state.toplevel.set_maximized();
-        } else {
-            state.toplevel.unset_maximized();
+        if let Some(toplevel) = state.surface_state.toplevel() {
+            if !state.maximized {
+                toplevel.set_maximized();
+            } else {
+                toplevel.unset_maximized();
+            }
         }
     }
 
     fn toggle_fullscreen(&self) {
-        let mut state = self.borrow_mut();
-        if !state.fullscreen {
-            state.toplevel.set_fullscreen(None);
-        } else {
-            state.toplevel.unset_fullscreen();
+        let mut state = self.borrow();
+        if let Some(toplevel) = state.surface_state.toplevel() {
+            if !state.fullscreen {
+                toplevel.set_fullscreen(None);
+            } else {
+                toplevel.unset_fullscreen();
+            }
         }
     }
 
@@ -1041,27 +1229,33 @@ impl PlatformWindow for WaylandWindow {
     fn show_window_menu(&self, position: Point<Pixels>) {
         let state = self.borrow();
         let serial = state.client.get_serial(SerialKind::MousePress);
-        state.toplevel.show_window_menu(
-            &state.globals.seat,
-            serial,
-            position.x.0 as i32,
-            position.y.0 as i32,
-        );
+        if let Some(toplevel) = state.surface_state.toplevel() {
+            toplevel.show_window_menu(
+                &state.globals.seat,
+                serial,
+                position.x.0 as i32,
+                position.y.0 as i32,
+            );
+        }
     }
 
     fn start_window_move(&self) {
         let state = self.borrow();
         let serial = state.client.get_serial(SerialKind::MousePress);
-        state.toplevel._move(&state.globals.seat, serial);
+        if let Some(toplevel) = state.surface_state.toplevel() {
+            toplevel._move(&state.globals.seat, serial);
+        }
     }
 
     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(),
-        )
+        if let Some(toplevel) = state.surface_state.toplevel() {
+            toplevel.resize(
+                &state.globals.seat,
+                state.client.get_serial(SerialKind::MousePress),
+                edge.to_xdg(),
+            )
+        }
     }
 
     fn window_decorations(&self) -> Decorations {
@@ -1077,7 +1271,7 @@ impl PlatformWindow for WaylandWindow {
     fn request_decorations(&self, decorations: WindowDecorations) {
         let mut state = self.borrow_mut();
         state.decorations = decorations;
-        if let Some(decoration) = state.decoration.as_ref() {
+        if let Some(decoration) = state.surface_state.decoration() {
             decoration.set_mode(decorations.to_xdg());
             update_window(state);
         }