Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(bevel): focus_sharpness as a config knob
The glint's falloff exponent was a #define, so trying a value meant a
rebuild — and it is a look setting, which is exactly the kind nobody
tunes if tuning costs a compile. Plumb it through the way focus_color
already goes: shader uniform, bevel_shader location, fx_render_bevel
option, scene-node field on the set_focus setter, LayoutConfig, and the
`bevel` node in config.kdl.
bevel ... focus_sharpness=(f64)3.0
Default 3.0, so absent config is exactly what shipped in a594a77.
Clamped to [1, 64]: below 1 the glint would spread wider than the rim's
own slope, which is what `thickness` is for.
Only the focus branch reads the uniform, so the desktop-grid bevels in
output.rs — which never call set_focus — are unaffected, and their
change-detection key does not need it.
Verified against the hardcoded builds from the sweep: default, 1.0 and
12.0 each reproduce their pinned-constant capture with zero differing
pixels.
Co-Authored-By: Claude Opus 5 <[email protected]>
scenefx/include/render/fx_renderer/shaders.h | 1 +
scenefx/include/scenefx/render/pass.h | 2 ++
scenefx/include/scenefx/types/wlr_scene.h | 4 +++-
scenefx/render/fx_renderer/fx_pass.c | 1 +
scenefx/render/fx_renderer/shaders.c | 1 +
scenefx/render/fx_renderer/shaders/bevel.frag | 9 +++++----
scenefx/types/scene/wlr_scene.c | 6 ++++--
src/server/config.rs | 22 ++++++++++++++++++++++
src/server/window.rs | 1 +
9 files changed, 40 insertions(+), 7 deletions(-)
diff --git a/scenefx/include/render/fx_renderer/shaders.h b/scenefx/include/render/fx_renderer/shaders.h
index ac77b75..10f28e8 100644
--- a/scenefx/include/render/fx_renderer/shaders.h
+++ b/scenefx/include/render/fx_renderer/shaders.h
@@ -177,6 +177,7 @@ struct bevel_shader {
GLint shoulder;
GLint focus;
GLint focus_color;
+ GLint focus_sharpness;
};
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 eb928c9..8914105 100644
--- a/scenefx/include/scenefx/render/pass.h
+++ b/scenefx/include/scenefx/render/pass.h
@@ -101,6 +101,8 @@ struct fx_render_bevel_options {
* in focus_color (the DE's focused-plate glint). */
float focus;
float focus_color[3];
+ /* Falloff exponent of the focus glint across the rim; 1 = linear. */
+ float focus_sharpness;
/* 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 c9161e7..9239a8d 100644
--- a/scenefx/include/scenefx/types/wlr_scene.h
+++ b/scenefx/include/scenefx/types/wlr_scene.h
@@ -212,6 +212,8 @@ struct wlr_scene_bevel {
*/
float focus;
float focus_color[3];
+ /** Falloff exponent of the focus glint across the rim; 1 = linear. */
+ float focus_sharpness;
};
struct wlr_scene_blur {
@@ -669,7 +671,7 @@ 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,
+void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus, float sharpness,
const float color[static 3]);
/**
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 8f943ff..0662dbf 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1162,6 +1162,7 @@ void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
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]);
+ glUniform1f(renderer->shaders.bevel.focus_sharpness, options->focus_sharpness);
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 cd2e6ef..6af59fc 100644
--- a/scenefx/render/fx_renderer/shaders.c
+++ b/scenefx/render/fx_renderer/shaders.c
@@ -369,6 +369,7 @@ bool link_bevel_program(struct bevel_shader *shader) {
shader->shade_intensity = glGetUniformLocation(prog, "shade_intensity");
shader->focus = glGetUniformLocation(prog, "focus");
shader->focus_color = glGetUniformLocation(prog, "focus_color");
+ shader->focus_sharpness = glGetUniformLocation(prog, "focus_sharpness");
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 8d15091..95c57e1 100644
--- a/scenefx/render/fx_renderer/shaders/bevel.frag
+++ b/scenefx/render/fx_renderer/shaders/bevel.frag
@@ -42,9 +42,10 @@ uniform vec3 focus_color;
// inside t=0 and is feathered by edge_aa, so it is already below 1.0 and the
// power pushes it down (measured peak, green channel: 242 at 1.0, 222 at 3.0,
// 198 at 6.0, 168 at 12.0). Past ~12 the curve stops narrowing and only dims:
-// 12 and 24 differ by 2/255. 3.0 keeps a defined edge without washing the
-// accent out to white.
-#define FOCUS_SHARPNESS 3.0
+// 12 and 24 differ by 2/255. The DE default is 3.0 — a defined edge without
+// washing the accent out to white — set by `bevel focus_sharpness=` in
+// config.kdl. Only the focus branch reads it.
+uniform float focus_sharpness;
// 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
@@ -114,7 +115,7 @@ void main() {
// power keeps the peak at the silhouette and falls away fast, which
// is what makes cce-ui's plate line read as a glint rather than a
// glow (shader2d.wgsl raises its profile to `shininess`).
- float h = light_intensity * pow(slope, FOCUS_SHARPNESS);
+ float h = light_intensity * pow(slope, max(focus_sharpness, 0.01));
// ADDITIVE: alpha 0 with premultiplied colour gives dst + rgb under
// the pass's GL_ONE/GL_ONE_MINUS_SRC_ALPHA blend, so the glint ADDS
// light to the window instead of cross-fading toward the accent.
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index 520ad96..7832189 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -1263,13 +1263,14 @@ 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,
+void wlr_scene_bevel_set_focus(struct wlr_scene_bevel *bevel, float focus, float sharpness,
const float color[static 3]) {
- if (bevel->focus == focus &&
+ if (bevel->focus == focus && bevel->focus_sharpness == sharpness &&
memcmp(bevel->focus_color, color, sizeof(bevel->focus_color)) == 0) {
return;
}
bevel->focus = focus;
+ bevel->focus_sharpness = sharpness;
memcpy(bevel->focus_color, color, sizeof(bevel->focus_color));
scene_node_update(&bevel->node, NULL);
}
@@ -2543,6 +2544,7 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
scene_bevel->focus_color[1],
scene_bevel->focus_color[2],
},
+ .focus_sharpness = scene_bevel->focus_sharpness,
.color = {
.r = scene_bevel->color[0],
.g = scene_bevel->color[1],
diff --git a/src/server/config.rs b/src/server/config.rs
index ae075d3..f5c76c7 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -92,6 +92,12 @@ pub struct Layout {
/// 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],
+ /// How sharply the focused rim's glint falls off across the bevel:
+ /// the exponent on the rim slope. 1 is a linear ramp over the whole
+ /// thickness (reads as a wash); higher concentrates the light at the
+ /// silhouette, at some cost in peak brightness, since the outermost
+ /// rendered sample is already below 1.0. Past ~12 it only dims.
+ pub bevel_focus_sharpness: f32,
/// Magnetic grid snap for interactive move/resize.
pub desktop_snap: bool,
/// Speed ramp + duration (ms) for the overview enter/exit transition.
@@ -214,6 +220,7 @@ impl Default for Layout {
bevel_shoulder: 0.55,
bevel_color: [1.0, 1.0, 1.0, 1.0],
bevel_focus_color: [0.35, 0.78, 0.78],
+ bevel_focus_sharpness: 3.0,
shadow_enabled: true,
shadow_sigma: 22.0,
shadow_color: [0.0, 0.0, 0.0, 0.55],
@@ -501,6 +508,8 @@ pub struct SurfaceConfig {
pub bevel_color: String,
#[serde(default = "default_bevel_focus_color")]
pub bevel_focus_color: String,
+ #[serde(default = "default_bevel_focus_sharpness")]
+ pub bevel_focus_sharpness: f64,
}
fn default_shadow_enabled() -> bool { true }
@@ -517,6 +526,7 @@ 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() }
+fn default_bevel_focus_sharpness() -> f64 { 3.0 }
/// 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.
@@ -577,6 +587,7 @@ impl Default for SurfaceConfig {
bevel_shoulder: default_bevel_shoulder(),
bevel_color: default_bevel_color(),
bevel_focus_color: default_bevel_focus_color(),
+ bevel_focus_sharpness: default_bevel_focus_sharpness(),
}
}
}
@@ -2045,6 +2056,13 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.bevel_color = val.to_string();
}
}
+ "focus_sharpness" => {
+ if let Some(val) = entry.value().as_f64() {
+ surface.bevel_focus_sharpness = val;
+ } else if let Some(val) = entry.value().as_i64() {
+ surface.bevel_focus_sharpness = val as f64;
+ }
+ }
"focus_color" => {
if let Some(val) = entry.value().as_string() {
surface.bevel_focus_color = val.to_string();
@@ -2359,6 +2377,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
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]];
+ // Clamped low at 1: below that the glint would spread WIDER than the
+ // rim's own slope, which is what `thickness` is for.
+ state.layout.bevel_focus_sharpness =
+ config.surface.bevel_focus_sharpness.clamp(1.0, 64.0) as f32;
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 2f48b95..6e39c21 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -3255,6 +3255,7 @@ impl Window {
ffi::wlr_scene_bevel_set_focus(
self.bevel,
if focused { 1.0 } else { 0.0 },
+ layout.bevel_focus_sharpness,
layout.bevel_focus_color.as_ptr(),
);
ffi::river_scene_node_set_position_if_changed(node, 0, 0);