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

commitcb0e66dc6984e046fdca2e4ebb6e20b17183b96b
parentf1d5e274eb
authorLucas Galante <[email protected]>
date2026-08-13 18:46
feat: bevel_apps — only bevel apps that don't bevel themselves

The bevel was gated on want_shadow, i.e. is_decorated_app, which has an
implicit "app_id starts with cce-" arm. Every cce-ui app already draws its
own bevelled rims, so the compositor was laying a second rim on top of
theirs.

Bevels now key on their own list: window_manager { bevel_apps "..." }, with
no implicit cce-* arm. Unset it defaults to the rounded_apps list, so the
apps you asked the compositor to round are the ones it bevels — and the
cce-* set, which is only implicit in is_decorated_app, is excluded either
way.

Corner rounding keeps its existing is_decorated_app gate; splitting that one
is a separate question (a cce-ui app that rounds itself is arguably in the
same position, and the compositor's clip radius need not match what the app
drew).

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

 src/server/config.rs         | 17 ++++++++++++++++-
 src/server/window.rs         | 16 ++++++++++++++--
 src/server/window_manager.rs | 11 +++++++++++
 3 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 250c46a..63a9979 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -319,6 +319,13 @@ pub struct WindowManagerConfig {
     /// decorated-window treatment: rounded corner clip, blur-behind, shadow.
     /// KDL: `rounded_apps "claude-desktop" "org.example.App"`.
     pub rounded_apps: Option<Vec<String>>,
+    /// Which apps the compositor draws an edge BEVEL on. Separate from
+    /// `rounded_apps` because drawing one is only right for apps that do not
+    /// bevel themselves — every cce-ui app already draws its own, so beveling
+    /// them compositor-side doubles the rim. Unset falls back to
+    /// `rounded_apps` (never the implicit cce-* set).
+    /// KDL: `bevel_apps "claude-desktop"`.
+    pub bevel_apps: Option<Vec<String>>,
 }
 
 #[derive(Debug, Deserialize, Clone, Default, PartialEq, Eq)]
@@ -2003,7 +2010,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
         let center_on_spawn = get_child_arg_bool_opt(node, "center_on_spawn");
         let corner_shape = get_child_arg_f64_opt(node, "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 });
+        let bevel_apps = get_child_args_string_vec_opt(node, "bevel_apps");
+        window_manager = Some(WindowManagerConfig { close_window, toggle_fullscreen, toggle_overview, window_switcher, window_switcher_prev, center_on_spawn, corner_shape, rounded_apps, bevel_apps });
     }
 
     Ok(Config {
@@ -2070,6 +2078,13 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
         .as_ref()
         .and_then(|wm| wm.rounded_apps.clone())
         .unwrap_or_default();
+    // Unset means "same apps as rounded_apps" — which deliberately excludes
+    // the implicit cce-* set, since those draw their own bevels.
+    state.bevel_apps = config
+        .window_manager
+        .as_ref()
+        .and_then(|wm| wm.bevel_apps.clone())
+        .unwrap_or_else(|| state.rounded_apps.clone());
 
     // 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
diff --git a/src/server/window.rs b/src/server/window.rs
index 10ec705..c33265f 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -2128,8 +2128,14 @@ impl Window {
                 (radius as f64 * self.scale) as i32,
             );
             let want_shadow = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
+                // The bevel keys on its OWN app list, not on is_decorated:
+                // every cce-ui app draws its own bevel, so a compositor one
+                // would sit on top of it.
+                let want_bevel = !is_status
+                    && !self.is_fullscreen()
+                    && (*self.server).wm.is_beveled_app(&app_id);
             self.update_shadow(width, height, radius, want_shadow);
-                self.update_bevel(width, height, radius, want_shadow);
+                self.update_bevel(width, height, radius, want_bevel);
             ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
 
             // Device px, like the blur radius above: the surface content is
@@ -2592,8 +2598,14 @@ impl Window {
                 // from before the gesture, so the punch-out overruns the shrunken
                 // window and swallows the shadow whole.
                 let want_shadow = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
+                // The bevel keys on its OWN app list, not on is_decorated:
+                // every cce-ui app draws its own bevel, so a compositor one
+                // would sit on top of it.
+                let want_bevel = !is_status
+                    && !self.is_fullscreen()
+                    && (*self.server).wm.is_beveled_app(&app_id);
                 self.update_shadow(width, height, radius, want_shadow);
-                self.update_bevel(width, height, radius, want_shadow);
+                self.update_bevel(width, height, radius, want_bevel);
             }
 
             self.scale_only_render_finish();
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 09e621a..46e09c1 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -180,6 +180,7 @@ pub struct WindowManager {
     /// treatment (rounded corner clip, blur-behind, shadow) alongside cce-* apps
     /// and SSD requesters.
     pub rounded_apps: Vec<String>,
+    pub bevel_apps: Vec<String>,
 }
 
 /// `CCE_DIRTY_BACKTRACE=1` — who called `dirty_windowing`. Separate from the
@@ -268,6 +269,7 @@ impl WindowManager {
         self.last_window_states = Vec::new();
         self.pending_placements = Vec::new();
         self.rounded_apps = Vec::new();
+        self.bevel_apps = Vec::new();
         self.shutting_down = false;
         self.layout = crate::config::Layout::default();
         self.output_scale = 1.0;
@@ -808,6 +810,15 @@ impl WindowManager {
         app_id.starts_with("cce-") || self.rounded_apps.iter().any(|a| a == app_id)
     }
 
+    /// Should the compositor draw an edge bevel on this app? Unlike
+    /// `is_decorated_app` there is NO implicit cce-* arm: every cce-ui app
+    /// draws its own bevel, and a second one from the compositor just doubles
+    /// the rim. Only apps named in `bevel_apps` (defaulting to `rounded_apps`)
+    /// get one.
+    pub fn is_beveled_app(&self, app_id: &str) -> bool {
+        self.bevel_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;