Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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,