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