diff --git a/Cargo.lock b/Cargo.lock index 765b34194c261c12c8fe76968200b19f50e954d0..46c90783f2939ff96ec647404fa081e2f6d85454 100644 --- a/Cargo.lock +++ b/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" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index bba9e3bd1cde783753eaf4b132c926292e769bbc..3bec72b2f2726d6373449f6c6828943d7c086909 100644 --- a/crates/gpui/Cargo.toml +++ b/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 } diff --git a/crates/gpui/examples/layer_shell.rs b/crates/gpui/examples/layer_shell.rs new file mode 100644 index 0000000000000000000000000000000000000000..51577b1b26491b8416a7df17ee310fd50dade8a3 --- /dev/null +++ b/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 { + 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) -> 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(); + }); + } +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index dd50a08c6b12ab198f1898ba79bae35969e6a5d0..c2bb1b3c6458efeceda46020c8270a306a2117d9 100644 --- a/crates/gpui/src/platform.rs +++ b/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 { } } +#[cfg(target_os = "windows")] +pub(crate) fn current_platform(_headless: bool) -> Rc { + 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 { - 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. diff --git a/crates/gpui/src/platform/linux.rs b/crates/gpui/src/platform/linux.rs index 5221f71f9970eb24508954304055acf974ed059d..f7d7ed0ebaa4165065f9963ee1be6d05601cf4ce 100644 --- a/crates/gpui/src/platform/linux.rs +++ b/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; diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index 487bc9f38c927609100a238ac4726c2aab3b87b0..366b5703e448522a59d397e00cbd268951cb1873 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/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; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 1ebdda3a266af0f9e8d82dabd5b36372e0972438..6461bf69738cfae2f791bf8eea69fe9a2a038a43 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/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, pub decoration_manager: Option, + pub layer_shell: Option, pub blur_manager: Option, pub text_input_manager: Option, 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> { 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 for WaylandClientStatePtr { } } +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + 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 for WaylandClientStatePtr { fn event( _: &mut Self, diff --git a/crates/gpui/src/platform/linux/wayland/layer_shell.rs b/crates/gpui/src/platform/linux/wayland/layer_shell.rs new file mode 100644 index 0000000000000000000000000000000000000000..0f165ed8e0ca2c1ec8d5b7c4cbdfea6cb5eec71b --- /dev/null +++ b/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 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 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 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, + /// The anchor point of the exclusive zone, will be determined using the anchor if left + /// unspecified. + pub exclusive_edge: Option, + /// 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; diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index a4b33e7a843c69644adb8580925a75ed4e799cdd..c02d1f3bc3d0d1ecf7589ae959f8c9b0e3f0fde5 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/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, app_id: Option, appearance: WindowAppearance, blur: Option, - toplevel: xdg_toplevel::XdgToplevel, viewport: Option, outputs: HashMap, display: Option<(ObjectId, Output)>, @@ -116,6 +116,161 @@ pub struct WaylandWindowState { client_inset: Option, } +pub enum WaylandSurfaceState { + Xdg(WaylandXdgSurfaceState), + LayerShell(WaylandLayerSurfaceState), +} + +impl WaylandSurfaceState { + fn new( + surface: &wl_surface::WlSurface, + globals: &Globals, + params: &WindowParams, + parent: Option, + ) -> anyhow::Result { + // For layer_shell windows, create a layer surface instead of an xdg surface + if let WindowKind::LayerShell(options) = ¶ms.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, +} + +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>, @@ -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, + surface_state: WaylandSurfaceState, appearance: WindowAppearance, viewport: Option, 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, ) -> 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, ¶ms, 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 { + 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) { 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); }