Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: the focused window wears the DE focus glint on its rim
The bevel shader gains a focus mode (focus + focus_color uniforms,
plumbed scene-node → pass options → uniforms): the familiar lit-edge
highlight, wrapped — the azimuth mask against the light drops, so every
edge shades as the lit one, in the accent color, with no shade side.
Same slope profile and shoulder, so band width matches the unfocused
chamfer exactly, and the shared corner_dist keeps it on the DE
superellipse.
update_bevel reads focus off the seats (the window's `activated` field
is a configure-time snapshot) on both render paths, and the node now
enables for ANY decorated window while focused — not just the bevel app
list: the focus branch draws only the glint, so it lays cleanly over a
cce-ui app's own client-side bevel instead of doubling its shading.
Unfocused windows are untouched.
Config: surface { bevel focus_color= } (default #59c7c7), live-tunable
via ccectl reload. The shader concat buffer grows to 16K with the
larger source.
Verified in a shadow session (never the live compositor): focused
cce-terminal shows the teal rim on all four sides sweeping the corner
arcs; unfocused windows unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
scenefx/include/render/fx_renderer/shaders.h | 2 ++
scenefx/include/scenefx/render/pass.h | 4 +++
scenefx/include/scenefx/types/wlr_scene.h | 9 +++++++
scenefx/render/fx_renderer/fx_pass.c | 3 +++
scenefx/render/fx_renderer/shaders.c | 4 ++-
scenefx/render/fx_renderer/shaders/bevel.frag | 24 +++++++++++++++---
scenefx/types/scene/wlr_scene.c | 17 +++++++++++++
src/server/config.rs | 15 ++++++++++++
src/server/window.rs | 35 ++++++++++++++++++++++++---
9 files changed, 104 insertions(+), 9 deletions(-)
diff --git a/scenefx/include/render/fx_renderer/shaders.h b/scenefx/include/render/fx_renderer/shaders.h
index 9350b13..ac77b75 100644
--- a/scenefx/include/render/fx_renderer/shaders.h
+++ b/scenefx/include/render/fx_renderer/shaders.h
@@ -175,6 +175,8 @@ struct bevel_shader {
GLint light_intensity;
GLint shade_intensity;
GLint shoulder;
+ GLint focus;
+ GLint focus_color;
};
bool link_bevel_program(struct bevel_shader *shader);
diff --git a/scenefx/include/scenefx/render/pass.h b/scenefx/include/scenefx/render/pass.h
index d9c9380..eb928c9 100644
--- a/scenefx/include/scenefx/render/pass.h
+++ b/scenefx/include/scenefx/render/pass.h
@@ -97,6 +97,10 @@ struct fx_render_bevel_options {
float shade_intensity;
/* 0 = hard flat chamfer, 1 = fully rounded shoulder. */
float shoulder;
+ /* Focus treatment: > 0 wraps the rim highlight around all four sides
+ * in focus_color (the DE's focused-plate glint). */
+ float focus;
+ float focus_color[3];
/* Tint of the highlight; alpha scales the whole effect. */
struct wlr_render_color color;
};
diff --git a/scenefx/include/scenefx/types/wlr_scene.h b/scenefx/include/scenefx/types/wlr_scene.h
index 440401d..c9161e7 100644
--- a/scenefx/include/scenefx/types/wlr_scene.h
+++ b/scenefx/include/scenefx/types/wlr_scene.h
@@ -205,6 +205,13 @@ struct wlr_scene_bevel {
float shoulder;
/** Highlight tint; alpha scales the whole effect. */
float color[4];
+ /**
+ * Focus treatment: > 0 wraps the rim highlight around all four sides
+ * (the azimuth mask against the light drops) in focus_color — the DE's
+ * focused-plate glint, on the window edge. 0 = the ordinary lit chamfer.
+ */
+ float focus;
+ float focus_color[3];
};
struct wlr_scene_blur {
@@ -662,6 +669,8 @@ void wlr_scene_droplet_set_silhouette(struct wlr_scene_droplet *droplet,
void wlr_scene_droplet_set_lens(struct wlr_scene_droplet *droplet,
float band_px, float refr, float ghost);
void wlr_scene_bevel_set_color(struct wlr_scene_bevel *bevel, const float color[static 4]);
+void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus,
+ const float color[static 3]);
/**
* Change the corner radius of an existing shadow node.
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 06fd116..9a97999 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1155,6 +1155,9 @@ void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
glUniform1f(renderer->shaders.bevel.light_intensity, options->light_intensity);
glUniform1f(renderer->shaders.bevel.shade_intensity, options->shade_intensity);
glUniform1f(renderer->shaders.bevel.shoulder, options->shoulder);
+ glUniform1f(renderer->shaders.bevel.focus, options->focus);
+ glUniform3f(renderer->shaders.bevel.focus_color,
+ options->focus_color[0], options->focus_color[1], options->focus_color[2]);
render(&box, &clip_region, renderer->shaders.bevel.pos_attrib);
pixman_region32_fini(&clip_region);
diff --git a/scenefx/render/fx_renderer/shaders.c b/scenefx/render/fx_renderer/shaders.c
index a4a83b0..cd2e6ef 100644
--- a/scenefx/render/fx_renderer/shaders.c
+++ b/scenefx/render/fx_renderer/shaders.c
@@ -347,7 +347,7 @@ bool link_box_shadow_program(struct box_shadow_shader *shader) {
bool link_bevel_program(struct bevel_shader *shader) {
// Same pairing as link_box_shadow_program: the rim's SDF is the shared
// corner routine, so the shape follows corner_shape like every other cut.
- GLchar bevel_src[8192];
+ GLchar bevel_src[16384];
snprintf(bevel_src, sizeof(bevel_src), "%s\n%s", bevel_frag_src,
corner_alpha_frag_src);
@@ -367,6 +367,8 @@ bool link_bevel_program(struct bevel_shader *shader) {
shader->light_dir = glGetUniformLocation(prog, "light_dir");
shader->light_intensity = glGetUniformLocation(prog, "light_intensity");
shader->shade_intensity = glGetUniformLocation(prog, "shade_intensity");
+ shader->focus = glGetUniformLocation(prog, "focus");
+ shader->focus_color = glGetUniformLocation(prog, "focus_color");
shader->shoulder = glGetUniformLocation(prog, "shoulder");
return true;
diff --git a/scenefx/render/fx_renderer/shaders/bevel.frag b/scenefx/render/fx_renderer/shaders/bevel.frag
index 6ded18b..387432d 100644
--- a/scenefx/render/fx_renderer/shaders/bevel.frag
+++ b/scenefx/render/fx_renderer/shaders/bevel.frag
@@ -29,6 +29,12 @@ uniform float shade_intensity;
// Shoulder: how much of the rim is the rounded roll-off vs the flat face.
// 0 = a hard flat chamfer, 1 = fully rounded shoulder.
uniform float shoulder;
+// Focus treatment: > 0 wraps the highlight around all four sides in
+// focus_color — the DE's focused-plate glint on the window edge. The azimuth
+// mask against the light drops, so every edge shades as the lit one; the
+// shade term goes with it (an all-around light casts no rim shadow).
+uniform float focus;
+uniform vec3 focus_color;
// Defined in corner_alpha.frag, which is concatenated after this source (the
// same way box_shadow.frag gets it). Using the shared routine rather than a
@@ -81,6 +87,20 @@ void main() {
// chamfer's tilt is what varies; the face itself is flat-on).
float facing = dot(grad, normalize(light_dir + vec2(1e-6)));
+ // Feather the very outer pixel so the rim doesn't alias against the
+ // window's own rounded edge.
+ float edge_aa = clamp(-dist, 0.0, 1.0);
+
+ if (focus > 0.5) {
+ // Focused window: the familiar lit-edge highlight, wrapped — every
+ // edge shades as the one facing the light, in the accent color, no
+ // shade side. Same slope profile, so band width and shoulder match
+ // the unfocused rim exactly.
+ float h = light_intensity * slope;
+ gl_FragColor = vec4(focus_color * h, h) * v_color.a * edge_aa;
+ return;
+ }
+
// Positive = lit side, negative = shaded side.
float lit = max(facing, 0.0) * light_intensity;
float shade = max(-facing, 0.0) * shade_intensity;
@@ -93,9 +113,5 @@ void main() {
vec3 rgb = v_color.rgb * highlight;
float alpha = highlight + shadow;
- // Feather the very outer pixel so the rim doesn't alias against the
- // window's own rounded edge.
- float edge_aa = clamp(-dist, 0.0, 1.0);
-
gl_FragColor = vec4(rgb, alpha) * v_color.a * edge_aa;
}
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index 1781f91..520ad96 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -1263,6 +1263,17 @@ void wlr_scene_bevel_set_color(struct wlr_scene_bevel *bevel, const float color[
scene_node_update(&bevel->node, NULL);
}
+void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus,
+ const float color[static 3]) {
+ if (bevel->focus == focus &&
+ memcmp(bevel->focus_color, color, sizeof(bevel->focus_color)) == 0) {
+ return;
+ }
+ bevel->focus = focus;
+ memcpy(bevel->focus_color, color, sizeof(bevel->focus_color));
+ scene_node_update(&bevel->node, NULL);
+}
+
void wlr_scene_shadow_set_size(struct wlr_scene_shadow *shadow, int width, int height) {
if (shadow->width == width && shadow->height == height) {
return;
@@ -2526,6 +2537,12 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
.light_intensity = scene_bevel->light_intensity,
.shade_intensity = scene_bevel->shade_intensity,
.shoulder = scene_bevel->shoulder,
+ .focus = scene_bevel->focus,
+ .focus_color = {
+ scene_bevel->focus_color[0],
+ scene_bevel->focus_color[1],
+ scene_bevel->focus_color[2],
+ },
.color = {
.r = scene_bevel->color[0],
.g = scene_bevel->color[1],
diff --git a/src/server/config.rs b/src/server/config.rs
index a319304..ae075d3 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -89,6 +89,9 @@ pub struct Layout {
pub bevel_shoulder: f32,
/// Highlight tint, premultiplied RGBA; alpha scales the whole effect.
pub bevel_color: [f32; 4],
+ /// Focused-window rim accent: the bevel highlight wraps all four sides
+ /// in this color for the focused window (the DE focus glint).
+ pub bevel_focus_color: [f32; 3],
/// Magnetic grid snap for interactive move/resize.
pub desktop_snap: bool,
/// Speed ramp + duration (ms) for the overview enter/exit transition.
@@ -210,6 +213,7 @@ impl Default for Layout {
bevel_shade_intensity: 0.5,
bevel_shoulder: 0.55,
bevel_color: [1.0, 1.0, 1.0, 1.0],
+ bevel_focus_color: [0.35, 0.78, 0.78],
shadow_enabled: true,
shadow_sigma: 22.0,
shadow_color: [0.0, 0.0, 0.0, 0.55],
@@ -495,6 +499,8 @@ pub struct SurfaceConfig {
pub bevel_shoulder: f64,
#[serde(default = "default_bevel_color")]
pub bevel_color: String,
+ #[serde(default = "default_bevel_focus_color")]
+ pub bevel_focus_color: String,
}
fn default_shadow_enabled() -> bool { true }
@@ -510,6 +516,7 @@ fn default_bevel_light_intensity() -> f64 { 0.6 }
fn default_bevel_shade_intensity() -> f64 { 0.5 }
fn default_bevel_shoulder() -> f64 { 0.55 }
fn default_bevel_color() -> String { "#ffffffff".to_string() }
+fn default_bevel_focus_color() -> String { "#59c7c7".to_string() }
/// Map a compass point to a unit vector pointing TOWARD the light, in screen
/// space (y down). Anything unrecognized keeps the DE's top-left default.
@@ -569,6 +576,7 @@ impl Default for SurfaceConfig {
bevel_shade_intensity: default_bevel_shade_intensity(),
bevel_shoulder: default_bevel_shoulder(),
bevel_color: default_bevel_color(),
+ bevel_focus_color: default_bevel_focus_color(),
}
}
}
@@ -2037,6 +2045,11 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.bevel_color = val.to_string();
}
}
+ "focus_color" => {
+ if let Some(val) = entry.value().as_string() {
+ surface.bevel_focus_color = val.to_string();
+ }
+ }
_ => {}
}
}
@@ -2344,6 +2357,8 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
state.layout.bevel_shade_intensity = config.surface.bevel_shade_intensity.clamp(0.0, 1.0) as f32;
state.layout.bevel_shoulder = config.surface.bevel_shoulder.clamp(0.0, 1.0) as f32;
state.layout.bevel_color = parse_hex_color_rgba(&config.surface.bevel_color);
+ let fc = parse_hex_color_rgba(&config.surface.bevel_focus_color);
+ state.layout.bevel_focus_color = [fc[0], fc[1], fc[2]];
for (key, val) in &config.env {
let expanded = expand_env_vars(val);
diff --git a/src/server/window.rs b/src/server/window.rs
index d5ca1d8..2f48b95 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -2631,7 +2631,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);
+ self.update_bevel(width, height, radius, want_bevel, want_shadow);
self.update_droplet(width, height);
ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
@@ -3102,7 +3102,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);
+ self.update_bevel(width, height, radius, want_bevel, want_shadow);
self.update_droplet(width, height);
}
@@ -3190,13 +3190,35 @@ impl Window {
/// The light direction is the DE's convention — the same top-left source
/// the drop shadow is offset away from — so a window reads as a slab lit
/// from the same place as everything else on the desktop.
- pub unsafe fn update_bevel(&self, width: i32, height: i32, radius: i32, want: bool) {
+ pub unsafe fn update_bevel(&self, width: i32, height: i32, radius: i32, want: bool, want_focus: bool) {
if self.bevel.is_null() {
return;
}
let node = &mut (*self.bevel).node as *mut ffi::wlr_scene_node;
let layout = &(*self.server).wm.layout;
- let enabled = want
+ // Focused-window treatment: the rim highlight wraps all four sides
+ // in the accent (the DE focus glint). Focus is read off the seats —
+ // the window's `activated` field is a configure-time snapshot, not
+ // live state — and this runs on both render paths, so a focus switch
+ // restyles on the next frame. A focused window that is NOT in the
+ // bevel app list still enables the node: the shader's focus branch
+ // draws ONLY the glint, so it lays cleanly over a cce-ui app's own
+ // client-side bevel instead of doubling its shading.
+ let mut focused = false;
+ {
+ let seats = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+ let mut curr = (*seats).next;
+ while curr != seats {
+ let seat = crate::container_of!(curr, crate::seat::Seat, link);
+ if let crate::seat::Focus::Window(w) = (*seat).focused {
+ if w == self as *const Window as *mut Window {
+ focused = true;
+ }
+ }
+ curr = (*curr).next;
+ }
+ }
+ let enabled = (want || (focused && want_focus))
&& layout.bevel_enabled
&& layout.bevel_thickness > 0.0
&& width > 0
@@ -3230,6 +3252,11 @@ impl Window {
);
ffi::wlr_scene_bevel_set_shoulder(self.bevel, layout.bevel_shoulder);
ffi::wlr_scene_bevel_set_color(self.bevel, layout.bevel_color.as_ptr());
+ ffi::wlr_scene_bevel_set_focus(
+ self.bevel,
+ if focused { 1.0 } else { 0.0 },
+ layout.bevel_focus_color.as_ptr(),
+ );
ffi::river_scene_node_set_position_if_changed(node, 0, 0);
}
/// Sync the droplet backdrop-refraction node for a droplet-styled status