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

commit063b24d010256cc7e8c102f9ca1a224fb6916c9f
parentb678fb813c
authorLucas Galante <[email protected]>
date2026-09-03 12:49
feat(shadow): `surface { shadow tiled=false }` drops the drop shadow on tiled windows

The scenefx drop shadow was all-or-nothing: `shadow enabled=false` took it
off floating windows too. A new `tiled` key under the same node keeps the
shadow for floating windows and suppresses it for Tiled ones (Maximized is
an alias of Tiled, so it follows). Default true — nothing changes for
existing configs.

The gate lives in `Window::wants_tiled_shadow` and is folded into both
want_shadow sites, so a float/tile toggle or a `reload` restyles on the
next arrange. The bevel's focus-glint argument used to be fed the shadow
decision; it now takes the ungated decorated-window predicate so the glint
is unaffected by the switch.

Verified headless (cce-shadow, debug build): with the key off a Tiled
cce-files reports its shadow node en=0 and Floating en=1; with the key
removed Tiled goes back to en=1.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FJYu168CifcJ25Bh6mQRPr

 src/server/config.rs | 15 +++++++++++++++
 src/server/window.rs | 23 +++++++++++++++++++----
 2 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index 0cecaae..c3ce9ea 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -90,6 +90,10 @@ pub struct Layout {
     /// `light_source_position` (down-right for the default top-left light).
     pub shadow_offset_x: i32,
     pub shadow_offset_y: i32,
+    /// Whether tiled (and maximized — an alias of Tiled) windows cast the
+    /// shadow at all. Off leaves shadows to floating windows only, so a
+    /// tiled layout reads as a flat sheet with no smearing across cell gaps.
+    pub shadow_tiled: bool,
     /// Edge bevel: a lit chamfer drawn around the INSIDE of a decorated
     /// window's edge (scenefx bevel node), lit from `bevel_light_*` — the
     /// same top-left source the drop shadow is offset away from.
@@ -247,6 +251,7 @@ impl Default for Layout {
             shadow_color: [0.0, 0.0, 0.0, 0.55],
             shadow_offset_x: 7,
             shadow_offset_y: 7,
+            shadow_tiled: true,
             desktop_snap: true,
             overview_anim: None,
             desktop_snap_threshold: 24.0,
@@ -521,6 +526,8 @@ pub struct SurfaceConfig {
     pub shadow_offset_x: i64,
     #[serde(default = "default_shadow_offset_y")]
     pub shadow_offset_y: i64,
+    #[serde(default = "default_shadow_tiled")]
+    pub shadow_tiled: bool,
     #[serde(default = "default_bevel_enabled")]
     pub bevel_enabled: bool,
     #[serde(default = "default_bevel_thickness")]
@@ -546,6 +553,7 @@ fn default_shadow_sigma() -> f64 { 22.0 }
 fn default_shadow_color() -> String { "#0000008c".to_string() }
 fn default_shadow_offset_x() -> i64 { 7 }
 fn default_shadow_offset_y() -> i64 { 7 }
+fn default_shadow_tiled() -> bool { true }
 fn default_bevel_enabled() -> bool { true }
 fn default_bevel_thickness() -> f64 { 10.0 }
 /// Compass point the light comes FROM, matching the shadow's top-left source.
@@ -612,6 +620,7 @@ impl Default for SurfaceConfig {
             shadow_color: default_shadow_color(),
             shadow_offset_x: default_shadow_offset_x(),
             shadow_offset_y: default_shadow_offset_y(),
+            shadow_tiled: default_shadow_tiled(),
             bevel_enabled: default_bevel_enabled(),
             bevel_thickness: default_bevel_thickness(),
             bevel_light: default_bevel_light(),
@@ -2172,6 +2181,11 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
                                             surface.shadow_offset_y = val;
                                         }
                                     }
+                                    "tiled" => {
+                                        if let Some(val) = entry.value().as_bool() {
+                                            surface.shadow_tiled = val;
+                                        }
+                                    }
                                     _ => {}
                                 }
                             }
@@ -2438,6 +2452,7 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
     state.layout.shadow_color = parse_hex_color_rgba(&config.surface.shadow_color);
     state.layout.shadow_offset_x = config.surface.shadow_offset_x as i32;
     state.layout.shadow_offset_y = config.surface.shadow_offset_y as i32;
+    state.layout.shadow_tiled = config.surface.shadow_tiled;
     state.layout.bevel_enabled = config.surface.bevel_enabled;
     state.layout.bevel_thickness = config.surface.bevel_thickness.max(0.0) as f32;
     let (bevel_lx, bevel_ly) = parse_light_direction(&config.surface.bevel_light);
diff --git a/src/server/window.rs b/src/server/window.rs
index 05ff877..30d6303 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -496,6 +496,15 @@ impl Window {
         false
     }
 
+    /// The `surface { shadow tiled=false }` switch: a Tiled window (which is
+    /// also what Maximized resolves to) casts no drop shadow when it is off.
+    /// Floating, popup and every other mode are unaffected. Evaluated on both
+    /// render paths, so a float/tile toggle restyles on the next arrange.
+    pub unsafe fn wants_tiled_shadow(&self) -> bool {
+        (*self.server).wm.layout.shadow_tiled
+            || self.tiling_mode != crate::tiling::TilingMode::Tiled
+    }
+
     pub unsafe fn is_fullscreen(&self) -> bool {
         self.tiling_mode == crate::tiling::TilingMode::Fullscreen
             || !self.wm_requested.fullscreen.is_null()
@@ -2660,7 +2669,10 @@ 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_decorated) && !self.is_fullscreen();
+            // The decorated-window predicate feeds two things: the shadow, and
+            // the bevel's focus glint. Only the shadow honours the tiled switch.
+            let want_decor = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
+            let want_shadow = want_decor && self.wants_tiled_shadow();
                 // 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.
@@ -2668,7 +2680,7 @@ impl Window {
                     && !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_bevel, want_shadow);
+                self.update_bevel(width, height, radius, want_bevel, want_decor);
                 self.update_droplet(width, height);
             ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
 
@@ -3131,7 +3143,10 @@ impl Window {
                 // the motion path touches them. Left stale they keep the scale
                 // 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 decorated-window predicate feeds two things: the shadow, and
+                // the bevel's focus glint. Only the shadow honours the tiled switch.
+                let want_decor = !is_status && (self.wm_requested.ssd || is_decorated) && !self.is_fullscreen();
+                let want_shadow = want_decor && self.wants_tiled_shadow();
                 // 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.
@@ -3139,7 +3154,7 @@ impl Window {
                     && !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_bevel, want_shadow);
+                self.update_bevel(width, height, radius, want_bevel, want_decor);
                 self.update_droplet(width, height);
             }