git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit4d98db3d2a1d2348719ba2e531a73bf823a26383
parent5deed3ece0
authorLucas Galante <[email protected]>
date2026-07-07 12:50
feat(engine): opt-in wlr-layer-shell support for Application

Add Application::layer() -> Option<LayerSettings> (default None). When set, the
engine creates a wlr-layer-shell surface (anchor/layer/exclusive-zone/margins/
keyboard-interactivity) instead of an xdg toplevel, with a LayerShellHandler
mirroring the xdg configure. xdg-only paths (interactive move, popups) are
guarded on window.is_some(), so existing xdg clients are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

 src/backend/window_runner.rs | 109 +++++++++++++++++++++++++++++++++++++------
 src/engine.rs                |   1 +
 2 files changed, 97 insertions(+), 13 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 0548ccd..dd641a1 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -3,6 +3,7 @@ use smithay_client_toolkit::{
     compositor::{CompositorHandler, CompositorState},
     delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
     delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output, delegate_xdg_popup,
+    delegate_layer,
     registry::{ProvidesRegistryState, RegistryState},
     output::{OutputHandler, OutputState},
     seat::{
@@ -15,6 +16,7 @@ use smithay_client_toolkit::{
             window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
             XdgShell,
         },
+        wlr_layer::{LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure},
         WaylandSurface,
     },
     shm::{Shm, ShmHandler},
@@ -1310,6 +1312,25 @@ pub struct WindowSettings {
     pub min_size: Option<(u32, u32)>,
 }
 
+// Re-export the wlr-layer-shell types apps need to describe a layer surface.
+pub use smithay_client_toolkit::shell::wlr_layer::{
+    Anchor as LayerAnchor, KeyboardInteractivity as LayerKeyboardInteractivity, Layer as LayerKind,
+};
+
+/// Opt-in configuration for running an [`Application`] on a wlr-layer-shell
+/// surface (panels, overlays, notifications) instead of an xdg toplevel.
+/// Return one from [`Application::layer`] to select layer-shell.
+#[derive(Debug, Clone)]
+pub struct LayerSettings {
+    pub layer: LayerKind,
+    pub anchor: LayerAnchor,
+    pub exclusive_zone: i32,
+    pub keyboard_interactivity: LayerKeyboardInteractivity,
+    /// (top, right, bottom, left) margins in logical pixels.
+    pub margin: (i32, i32, i32, i32),
+    pub namespace: String,
+}
+
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct LogicalPosition {
     pub x: f32,
@@ -1343,6 +1364,11 @@ pub trait Application: Sized + 'static {
 
     fn new(qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self;
     fn settings(&self) -> WindowSettings;
+    /// Return `Some(..)` to run on a wlr-layer-shell surface (overlay/panel)
+    /// instead of an xdg toplevel. Defaults to `None` (a normal window).
+    fn layer(&self) -> Option<LayerSettings> {
+        None
+    }
     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
     fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64);
@@ -1503,14 +1529,16 @@ pub struct EngineState<A: Application> {
     pub registry_state: RegistryState,
     pub compositor_state: CompositorState,
     pub xdg_shell_state: XdgShell,
+    pub layer_shell_state: Option<LayerShell>,
     pub shm_state: Shm,
     pub seat_state: SeatState,
     pub output_state: OutputState,
     pub seats: Vec<wl_seat::WlSeat>,
     pub pointer: Option<ThemedPointer>,
     pub keyboard: Option<wl_keyboard::WlKeyboard>,
-    
+
     pub window: Option<XdgWindow>,
+    pub layer_surface: Option<LayerSurface>,
     pub surface: Option<wl_surface::WlSurface>,
     
     pub inner: Option<A>,
@@ -2102,6 +2130,35 @@ impl<A: Application> WindowHandler for EngineState<A> {
     }
 }
 
+impl<A: Application> LayerShellHandler for EngineState<A> {
+    fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
+        self.exit = true;
+    }
+
+    fn configure(
+        &mut self,
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+        _layer: &LayerSurface,
+        configure: LayerSurfaceConfigure,
+        _serial: u32,
+    ) {
+        // new_size is in logical pixels; 0 means "client decides", so fall back
+        // to the app's requested size (mirrors the xdg WindowHandler above).
+        let (w, h) = configure.new_size;
+        if w > 0 && h > 0 {
+            self.resize(w as f32, h as f32);
+        } else {
+            let settings = self.inner.as_ref().unwrap().settings();
+            self.resize(settings.width as f32, settings.height as f32);
+        }
+        self.redraw = true;
+        self.frame_callback_pending = false;
+        self.first_configure_received = true;
+        self.just_configured = true;
+    }
+}
+
 impl<A: Application> SeatHandler for EngineState<A> {
     fn seat_state(&mut self) -> &mut SeatState {
         &mut self.seat_state
@@ -2633,6 +2690,7 @@ delegate_compositor!(@<A: Application> EngineState<A>);
 delegate_xdg_shell!(@<A: Application> EngineState<A>);
 delegate_xdg_window!(@<A: Application> EngineState<A>);
 delegate_xdg_popup!(@<A: Application> EngineState<A>);
+delegate_layer!(@<A: Application> EngineState<A>);
 delegate_shm!(@<A: Application> EngineState<A>);
 delegate_seat!(@<A: Application> EngineState<A>);
 delegate_pointer!(@<A: Application> EngineState<A>);
@@ -2735,6 +2793,7 @@ pub fn run<A: Application>() {
 
     let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
     let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
+    let layer_shell_state = LayerShell::bind(&globals, &qh).ok();
     let shm_state = Shm::bind(&globals, &qh).unwrap();
     let seat_state = SeatState::new(&globals, &qh);
     let output_state = OutputState::new(&globals, &qh);
@@ -2746,6 +2805,7 @@ pub fn run<A: Application>() {
         registry_state: RegistryState::new(&globals),
         compositor_state,
         xdg_shell_state,
+        layer_shell_state,
         shm_state,
         seat_state,
         output_state,
@@ -2753,6 +2813,7 @@ pub fn run<A: Application>() {
         pointer: None,
         keyboard: None,
         window: None,
+        layer_surface: None,
         surface: None,
         inner: None,
         wgpu_adapter: None,
@@ -2808,18 +2869,40 @@ pub fn run<A: Application>() {
         region.destroy();
     }
 
-    let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
-    window.set_title(&settings.title);
-    window.set_app_id(&settings.app_id);
-    if settings.fullscreen {
-        window.set_fullscreen(None);
-    }
-    if let Some((min_w, min_h)) = settings.min_size {
-        window.set_min_size(Some((min_w, min_h)));
+    let layer_settings = engine_state.inner.as_ref().unwrap().layer();
+    if let Some(ls) = layer_settings {
+        let layer_shell = engine_state
+            .layer_shell_state
+            .as_ref()
+            .expect("compositor does not support wlr-layer-shell");
+        let layer_surface = layer_shell.create_layer_surface(
+            &qh,
+            surface.clone(),
+            ls.layer,
+            Some(ls.namespace.clone()),
+            None,
+        );
+        layer_surface.set_anchor(ls.anchor);
+        layer_surface.set_exclusive_zone(ls.exclusive_zone);
+        layer_surface.set_keyboard_interactivity(ls.keyboard_interactivity);
+        let (t, r, b, l) = ls.margin;
+        layer_surface.set_margin(t, r, b, l);
+        layer_surface.set_size(settings.width, settings.height);
+        layer_surface.commit();
+        engine_state.layer_surface = Some(layer_surface);
+    } else {
+        let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
+        window.set_title(&settings.title);
+        window.set_app_id(&settings.app_id);
+        if settings.fullscreen {
+            window.set_fullscreen(None);
+        }
+        if let Some((min_w, min_h)) = settings.min_size {
+            window.set_min_size(Some((min_w, min_h)));
+        }
+        window.commit();
+        engine_state.window = Some(window);
     }
-    window.commit();
-
-    engine_state.window = Some(window);
     engine_state.surface = Some(surface);
 
     pollster::block_on(engine_state.init_gpu(&conn, settings.width as f32, settings.height as f32));
@@ -2946,7 +3029,7 @@ pub fn run<A: Application>() {
             };
             pw = pw.ceil();
             ph = ph.ceil();
-            if engine_state.active_popup.is_none() {
+            if engine_state.active_popup.is_none() && engine_state.window.is_some() {
                 let positioner = XdgPositioner::new(&engine_state.xdg_shell_state).unwrap();
                 positioner.set_size(pw as i32, ph as i32);
                 
diff --git a/src/engine.rs b/src/engine.rs
index 6addf21..d036b44 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -1,6 +1,7 @@
 pub use crate::backend::window_runner::{
     ActivePopup, Vertex, LineCap, WindowSettings, LogicalPosition, LogicalSize,
     RenderContext, Application, PressedKey, EngineState, run,
+    LayerSettings, LayerKind, LayerAnchor, LayerKeyboardInteractivity,
     quad_vertices, quad_vertices_with_clip, quad_vertices_clipped, line_vertices,
     vector_vertices, rounded_rect_vertices_corners, push_rounded_rect_vertices_corners,
     rounded_rect_vertices, push_rounded_rect_vertices, plate_bevel_vertices,