git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitae01e2b00ca0f510e6efb80469ff7dcc1cfcc1b2
parent3c0e703b94
authorLucas Galante <[email protected]>
date2026-08-12 12:55
feat: rounded_apps allowlist — decorated treatment for foreign apps

window_manager.rounded_apps lists app_ids that get the full decorated
treatment (rounded corner clip, blur-behind, drop shadow) alongside
cce-* apps and SSD requesters. The eligibility predicate is centralized
in WindowManager::is_decorated_app so the mirrored render sites cannot
drift.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/config.rs         | 59 +++++++++++++++++++++++++++++++++++++++++++-
 src/server/window.rs         | 20 +++++++--------
 src/server/window_manager.rs | 14 +++++++++++
 src/server/xdg_toplevel.rs   |  6 ++---
 4 files changed, 85 insertions(+), 14 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 87a5b3b..f298e95 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -291,6 +291,10 @@ pub struct WindowManagerConfig {
     /// squircle. The same `window_manager.corner_shape` key the cce-ui
     /// clients read, so the compositor's cut lands on the corners they draw.
     pub corner_shape: Option<f64>,
+    /// Extra app_ids (beyond cce-* apps and SSD requesters) that get the full
+    /// decorated-window treatment: rounded corner clip, blur-behind, shadow.
+    /// KDL: `rounded_apps "claude-desktop" "org.example.App"`.
+    pub rounded_apps: Option<Vec<String>>,
 }
 
 #[derive(Debug, Deserialize, Clone, Default, PartialEq, Eq)]
@@ -1048,6 +1052,26 @@ fn get_child_arg_vec2i_opt(node: &kdl::KdlNode, child_name: &str) -> Option<[i32
 }
 
 
+/// All positional string args of a child node, e.g. `rounded_apps "a" "b"`.
+/// `Some` when the child node is present (even with no args), `None` when absent.
+fn get_child_args_string_vec_opt(node: &kdl::KdlNode, child_name: &str) -> Option<Vec<String>> {
+    if let Some(children) = node.children() {
+        for child in children.nodes() {
+            if child.name().value() == child_name {
+                return Some(
+                    child
+                        .entries()
+                        .iter()
+                        .filter(|e| e.name().is_none())
+                        .filter_map(|e| e.value().as_string().map(|s| s.to_string()))
+                        .collect(),
+                );
+            }
+        }
+    }
+    None
+}
+
 fn get_child_arg_string_opt(node: &kdl::KdlNode, child_name: &str) -> Option<String> {
     if let Some(children) = node.children() {
         for child in children.nodes() {
@@ -1862,7 +1886,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
         let window_switcher_prev = get_child_arg_string_opt(node, "window_switcher_prev");
         let center_on_spawn = get_child_arg_bool_opt(node, "center_on_spawn");
         let corner_shape = get_child_arg_f64_opt(node, "corner_shape");
-        window_manager = Some(WindowManagerConfig { close_window, toggle_fullscreen, toggle_overview, window_switcher, window_switcher_prev, center_on_spawn, corner_shape });
+        let rounded_apps = get_child_args_string_vec_opt(node, "rounded_apps");
+        window_manager = Some(WindowManagerConfig { close_window, toggle_fullscreen, toggle_overview, window_switcher, window_switcher_prev, center_on_spawn, corner_shape, rounded_apps });
     }
 
     Ok(Config {
@@ -1924,6 +1949,11 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
         .as_ref()
         .and_then(|wm| wm.center_on_spawn)
         .unwrap_or(true);
+    state.rounded_apps = config
+        .window_manager
+        .as_ref()
+        .and_then(|wm| wm.rounded_apps.clone())
+        .unwrap_or_default();
 
     // Feed scenefx's rounded-corner shaders the DE-wide corner-shape exponent
     // (clamped like cce-ui's corner_shape()). Plain C state, safe pre-renderer
@@ -2338,6 +2368,33 @@ mod tests {
         assert!(parse_kdl_config("layout {\n gap 4\n}").unwrap().window_manager.is_none());
     }
 
+    #[test]
+    fn test_kdl_window_manager_rounded_apps() {
+        let listed = parse_kdl_config(
+            r#"
+            window_manager {
+                rounded_apps "claude-desktop" "org.keepassxc.KeePassXC"
+            }
+        "#,
+        )
+        .unwrap();
+        assert_eq!(
+            listed.window_manager.unwrap().rounded_apps,
+            Some(vec!["claude-desktop".to_string(), "org.keepassxc.KeePassXC".to_string()])
+        );
+
+        // Absent means "unset": the apply step reads it as an empty allowlist.
+        let absent = parse_kdl_config(
+            r#"
+            window_manager {
+                center_on_spawn (bool)true
+            }
+        "#,
+        )
+        .unwrap();
+        assert_eq!(absent.window_manager.unwrap().rounded_apps, None);
+    }
+
     #[test]
     fn test_kdl_window_manager_corner_shape() {
         let set = parse_kdl_config(
diff --git a/src/server/window.rs b/src/server/window.rs
index f91b724..21a3b51 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -2020,8 +2020,8 @@ impl Window {
             let app_id = self.get_app_id_string().unwrap_or_default();
             let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
                             app_id.starts_with("cce-status");
-            let is_cce_app = app_id.starts_with("cce-");
-            let blur_enabled = requested.blur && (self.wm_requested.ssd || is_cce_app || is_status);
+            let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
+            let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status);
             let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
             if is_status {
                 ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
@@ -2042,7 +2042,7 @@ impl Window {
                 // carves visible sweeps into an EXPANDED segment's in-surface
                 // menu box once the cap stops binding.
                 0
-            } else if self.wm_requested.ssd || is_cce_app {
+            } else if self.wm_requested.ssd || is_decorated {
                 (*self.server).wm.layout.backplate_corner_radius
             } else {
                 0
@@ -2108,7 +2108,7 @@ impl Window {
                 // (cf. the window_background rect, which scales it the same way).
                 (radius as f64 * self.scale) as i32,
             );
-            let want_shadow = !is_status && (self.wm_requested.ssd || is_cce_app) && !self.is_fullscreen();
+            let want_shadow = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
             self.update_shadow(width, height, radius, want_shadow);
             ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
 
@@ -2480,8 +2480,8 @@ impl Window {
             if app_id.starts_with("cce-") {
                 let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
                                 app_id.starts_with("cce-status");
-                let is_cce_app = app_id.starts_with("cce-");
-                let blur_enabled = requested.blur && (self.wm_requested.ssd || is_cce_app || is_status);
+                let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
+                let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status);
                 let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
                 if is_status {
                     ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
@@ -2499,7 +2499,7 @@ impl Window {
                     // Same status exemption as set_rendering_state — the two
                     // paths drive the same nodes and must agree.
                     0
-                } else if self.wm_requested.ssd || is_cce_app {
+                } else if self.wm_requested.ssd || is_decorated {
                     (*self.server).wm.layout.backplate_corner_radius
                 } else {
                     0
@@ -2546,7 +2546,7 @@ impl Window {
                     height,
                     (radius as f64 * self.scale) as i32,
                 );
-                let want_shadow = !is_status && (self.wm_requested.ssd || is_cce_app) && !self.is_fullscreen();
+                let want_shadow = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
                 self.update_shadow(width, height, radius, want_shadow);
             } else {
                 // Tearing the blur down: radius is irrelevant, the nodes are destroyed.
@@ -3884,8 +3884,8 @@ impl Decoration {
         if is_status {
             ignore_transparent = (*server).wm.layout.status_backdrop_blur_ignore_transparent;
         }
-        let is_cce_app = app_id.starts_with("cce-");
-        let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_cce_app || is_status);
+        let is_decorated = (*server).wm.is_decorated_app(&app_id);
+        let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_decorated || is_status);
         // Radius 0 preserves existing behaviour on the layer-surface path (see layer_shell.rs)
         // — it never had a blur radius applied, and this fix is scoped to toplevels.
         ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, blur_enabled, (*server).wm.layout.scenefx_optimized_blur, ignore_transparent, 0, 0, 0, 0, 0);
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 7cc7794..5bf12c9 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -169,6 +169,10 @@ pub struct WindowManager {
     /// over to it when it takes focus. Off, the desk stays put and the window opens wherever
     /// the layout placed it. Focus-follow panning between EXISTING windows is unaffected.
     pub center_on_spawn: bool,
+    /// `window_manager.rounded_apps`: extra app_ids that get the decorated-window
+    /// treatment (rounded corner clip, blur-behind, shadow) alongside cce-* apps
+    /// and SSD requesters.
+    pub rounded_apps: Vec<String>,
 }
 
 impl WindowManager {
@@ -231,6 +235,7 @@ impl WindowManager {
         self.restore_queue = Vec::new();
         self.last_window_states = Vec::new();
         self.pending_placements = Vec::new();
+        self.rounded_apps = Vec::new();
         self.shutting_down = false;
         self.layout = crate::config::Layout::default();
         self.output_scale = 1.0;
@@ -759,6 +764,15 @@ impl WindowManager {
         }
     }
 
+    /// Whether an app_id gets the decorated-window treatment (rounded corner
+    /// clip, blur-behind, drop shadow) without requesting SSD: every cce app,
+    /// plus the `window_manager.rounded_apps` config allowlist. The one
+    /// predicate behind every radius/blur/shadow decision — the mirrored
+    /// render sites must all agree or the effects visibly disagree per pass.
+    pub fn is_decorated_app(&self, app_id: &str) -> bool {
+        app_id.starts_with("cce-") || self.rounded_apps.iter().any(|a| a == app_id)
+    }
+
     pub unsafe fn match_and_remove_restore_state(&mut self, app_id: &str, title: &str) -> Option<SavedWindowState> {
         if app_id.is_empty() {
             return None;
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 3043b82..7d68c87 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -564,7 +564,7 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
     let scale = (*window).scale;
     let is_status = (*window).tiling_mode == crate::tiling::TilingMode::Status ||
                     app_id.starts_with("cce-status");
-    let is_cce_app = app_id.starts_with("cce-");
+    let is_decorated = (*(*window).server).wm.is_decorated_app(&app_id);
     // Status segments are SELF-sizing (their bounds track their own box), so
     // the geometry of the commit being handled is the truth. `rendering_sent`
     // is a render-start snapshot that lags a contract commit by a render pass
@@ -593,7 +593,7 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
         // Same status exemption as Window::set_rendering_state (part of the
         // mirror): status segments draw their own module-box corners.
         0
-    } else if (*window).wm_requested.ssd || is_cce_app {
+    } else if (*window).wm_requested.ssd || is_decorated {
         (*(*window).server).wm.layout.backplate_corner_radius
     } else {
         0
@@ -609,7 +609,7 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
     } else {
         (*(*window).server).wm.layout.scenefx_optimized_blur
     };
-    let blur_enabled = (*window).rendering_requested.blur && ((*window).wm_requested.ssd || is_cce_app || is_status);
+    let blur_enabled = (*window).rendering_requested.blur && ((*window).wm_requested.ssd || is_decorated || is_status);
     ffi::river_scene_node_enable_blur(
         (*window).tree as *mut ffi::wlr_scene_node,
         blur_enabled,