Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: droplet backdrop refraction — the bar's drops bend what's behind them
scenefx gains WLR_SCENE_NODE_DROPLET alongside the bevel node it is
modelled on. The node re-renders the region behind a droplet-styled
status segment through the drop's lens: inside the drop silhouette —
the SAME superellipse sheet + bottom-arc smax shape the cce-ui client
draws, resolved from the same module { droplet } spec via
DropletSpec::parse/resolve_silhouette so the two sides cannot drift —
the shader samples scenefx's unblurred optimized-blur snapshot with a
rim-weighted offset along the SDF gradient (the background bends
around the drop edge) plus a faint inverted, minified sample about
the drop center (the upside-down image a real hanging drop shows).
New spec knobs refr (rim offset px) and ghost (0-1) gate it; both 0
keeps the node disabled and nothing changes.
The node draws BENEATH the segment's surfaces (created before them,
like the shadow), so the client's translucent tint, gleam and text
composite over the bent backdrop. When the node is live the segment's
per-window blur yields (droplet_backdrop_on) — the blur pass would
composite the UNREFRACTED cached backdrop over the lens. update_droplet
runs from BOTH render paths, like update_bevel (the one-path freeze
trap). Segments taller than the bar (expanded menus) keep the node
off. Config: layout.status_droplet carries the bar app config's
module { droplet } string, re-read on ccectl reload.
Verified headless (shadow instance agent-2): with refr=25/ghost=0.8 a
desktop grid rail visibly refracts into and magnifies inside the clock
drop while refr=0 shows the flat frosted capsule; sane values (refr=8
ghost=0.35) give a subtle lens. cce-fx config tests pass. The live
session picks this up on its next session restart.
Co-Authored-By: Claude Fable 5 <[email protected]>
scenefx/include/render/fx_renderer/fx_renderer.h | 1 +
scenefx/include/render/fx_renderer/shaders.h | 20 ++++
scenefx/include/scenefx/render/pass.h | 28 ++++++
scenefx/include/scenefx/types/wlr_scene.h | 34 +++++++
scenefx/render/fx_renderer/fx_pass.c | 70 ++++++++++++++
scenefx/render/fx_renderer/fx_renderer.c | 7 ++
scenefx/render/fx_renderer/shaders.c | 25 +++++
scenefx/render/fx_renderer/shaders/droplet.frag | 114 +++++++++++++++++++++++
scenefx/render/fx_renderer/shaders/meson.build | 1 +
scenefx/types/scene/wlr_scene.c | 88 ++++++++++++++++-
src/server/config.rs | 32 +++++++
src/server/window.rs | 91 +++++++++++++++++-
12 files changed, 507 insertions(+), 4 deletions(-)
diff --git a/scenefx/include/render/fx_renderer/fx_renderer.h b/scenefx/include/render/fx_renderer/fx_renderer.h
index c4700c8..c4d492a 100644
--- a/scenefx/include/render/fx_renderer/fx_renderer.h
+++ b/scenefx/include/render/fx_renderer/fx_renderer.h
@@ -201,6 +201,7 @@ struct fx_renderer {
struct box_shadow_shader box_shadow;
struct bevel_shader bevel;
+ struct droplet_shader droplet;
struct blur_shader blur1;
struct blur_shader blur2;
struct blur_effects_shader blur_effects;
diff --git a/scenefx/include/render/fx_renderer/shaders.h b/scenefx/include/render/fx_renderer/shaders.h
index 3534042..9350b13 100644
--- a/scenefx/include/render/fx_renderer/shaders.h
+++ b/scenefx/include/render/fx_renderer/shaders.h
@@ -179,6 +179,26 @@ struct bevel_shader {
bool link_bevel_program(struct bevel_shader *shader);
+struct droplet_shader {
+ GLuint program;
+ GLint proj;
+ GLint pos_attrib;
+ GLint tex;
+ GLint tex_size;
+ GLint position;
+ GLint size;
+ GLint attach_r;
+ GLint sheet_r;
+ GLint bow_rise;
+ GLint blend_k;
+ GLint curve;
+ GLint band_px;
+ GLint refr;
+ GLint ghost;
+};
+
+bool link_droplet_program(struct droplet_shader *shader);
+
struct blur_shader {
GLuint program;
GLint proj;
diff --git a/scenefx/include/scenefx/render/pass.h b/scenefx/include/scenefx/render/pass.h
index 5c58dda..d9c9380 100644
--- a/scenefx/include/scenefx/render/pass.h
+++ b/scenefx/include/scenefx/render/pass.h
@@ -101,6 +101,26 @@ struct fx_render_bevel_options {
struct wlr_render_color color;
};
+struct fx_render_droplet_options {
+ struct wlr_box box;
+ /* Clip region, leave NULL to disable clipping */
+ const pixman_region32_t *clip;
+
+ /* Silhouette, all px at output scale: top/bottom corner radii, the
+ * bottom-arc edge rise (0 = flat run), the smooth-intersection blend,
+ * and the corner-shape exponent (2 = circular). */
+ float attach_r;
+ float sheet_r;
+ float bow_rise;
+ float blend_k;
+ float curve;
+ /* Refraction: falloff band px, rim offset px (sign flips direction),
+ * inverted-lens ghost strength 0-1. */
+ float band_px;
+ float refr;
+ float ghost;
+};
+
struct fx_render_blur_pass_options {
struct fx_render_texture_options tex_options;
struct fx_framebuffer *current_buffer;
@@ -163,6 +183,14 @@ void fx_render_pass_add_box_shadow(struct fx_gles_render_pass *pass,
void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
const struct fx_render_bevel_options *options);
+/**
+ * Render a droplet backdrop: the unblurred below-layer snapshot re-rendered
+ * through a water drop's lens (rim refraction + inverted ghost) inside the
+ * drop's silhouette. No-op when the snapshot buffer does not exist.
+ */
+void fx_render_pass_add_droplet(struct fx_gles_render_pass *pass,
+ const struct fx_render_droplet_options *options);
+
/**
* Render blur.
*/
diff --git a/scenefx/include/scenefx/types/wlr_scene.h b/scenefx/include/scenefx/types/wlr_scene.h
index 9c7e1e4..440401d 100644
--- a/scenefx/include/scenefx/types/wlr_scene.h
+++ b/scenefx/include/scenefx/types/wlr_scene.h
@@ -62,6 +62,7 @@ enum wlr_scene_node_type {
WLR_SCENE_NODE_BUFFER,
WLR_SCENE_NODE_SHADOW,
WLR_SCENE_NODE_BEVEL,
+ WLR_SCENE_NODE_DROPLET,
WLR_SCENE_NODE_OPTIMIZED_BLUR,
WLR_SCENE_NODE_BLUR,
};
@@ -172,6 +173,24 @@ struct wlr_scene_shadow {
};
/** A lit chamfer around the inside of a rounded rect. */
+/** A water-drop backdrop lens for a status segment: refracts (and ghosts)
+ * the unblurred below-layer snapshot inside the drop's silhouette. Drawn
+ * BENEATH the segment's surface; the client's translucent drop composites
+ * on top. All silhouette/refraction fields are logical px (scaled with the
+ * output at render time). */
+struct wlr_scene_droplet {
+ struct wlr_scene_node node;
+ int width, height;
+ float attach_r;
+ float sheet_r;
+ float bow_rise;
+ float blend_k;
+ float curve;
+ float band_px;
+ float refr;
+ float ghost;
+};
+
struct wlr_scene_bevel {
struct wlr_scene_node node;
int width, height;
@@ -541,6 +560,11 @@ struct wlr_scene_shadow *wlr_scene_shadow_from_node(struct wlr_scene_node *node)
*/
struct wlr_scene_bevel *wlr_scene_bevel_from_node(struct wlr_scene_node *node);
+/**
+ * If this node represents a wlr_scene_droplet, that structure is returned.
+ */
+struct wlr_scene_droplet *wlr_scene_droplet_from_node(struct wlr_scene_node *node);
+
struct wlr_scene_blur *wlr_scene_blur_from_node(struct wlr_scene_node *node);
/**
@@ -627,6 +651,16 @@ void wlr_scene_bevel_set_thickness(struct wlr_scene_bevel *bevel, float thicknes
void wlr_scene_bevel_set_light(struct wlr_scene_bevel *bevel, float dir_x, float dir_y,
float light_intensity, float shade_intensity);
void wlr_scene_bevel_set_shoulder(struct wlr_scene_bevel *bevel, float shoulder);
+
+struct wlr_scene_droplet *wlr_scene_droplet_create(struct wlr_scene_tree *parent,
+ int width, int height);
+void wlr_scene_droplet_set_size(struct wlr_scene_droplet *droplet, int width, int height);
+/** Silhouette: top/bottom corner radii, bottom-arc rise, blend, exponent. */
+void wlr_scene_droplet_set_silhouette(struct wlr_scene_droplet *droplet,
+ float attach_r, float sheet_r, float bow_rise, float blend_k, float curve);
+/** Lens: falloff band px, rim refraction px, inverted-ghost strength 0-1. */
+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]);
/**
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index f1a8738..9fff1f9 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1164,6 +1164,76 @@ void fx_render_pass_add_bevel(struct fx_gles_render_pass *pass,
pop_fx_debug(renderer);
}
+void fx_render_pass_add_droplet(struct fx_gles_render_pass *pass,
+ const struct fx_render_droplet_options *options) {
+ struct fx_renderer *renderer = pass->buffer->renderer;
+
+ struct wlr_box box = options->box;
+ assert(box.width > 0 && box.height > 0);
+
+ // The lens samples the UNBLURRED snapshot of everything beneath the bar
+ // layer — the same capture the optimized blur bakes from. Without it
+ // (blur disabled DE-wide) there is nothing to refract; the client's own
+ // translucent drop still renders, so skipping is a graceful degrade.
+ if (pass->fx_offscreen_buffers == NULL) {
+ return;
+ }
+ struct fx_framebuffer *snap = pass->fx_offscreen_buffers->optimized_no_blur_buffer;
+ if (snap == NULL || snap->buffer == NULL) {
+ return;
+ }
+ struct wlr_texture *wlr_texture =
+ fx_texture_from_buffer(&renderer->wlr_renderer, snap->buffer);
+ if (wlr_texture == NULL) {
+ return;
+ }
+ struct fx_texture *texture = fx_get_texture(wlr_texture);
+
+ pixman_region32_t clip_region;
+ if (options->clip) {
+ pixman_region32_init(&clip_region);
+ pixman_region32_copy(&clip_region, options->clip);
+ } else {
+ pixman_region32_init_rect(&clip_region, box.x, box.y, box.width, box.height);
+ }
+
+ push_fx_debug(renderer);
+
+ // The refracted backdrop is opaque inside the silhouette (it REPLACES
+ // what the drop covers); the feathered rim blends source-over.
+ setup_blending(WLR_RENDER_BLEND_MODE_PREMULTIPLIED);
+
+ struct droplet_shader *shader = &renderer->shaders.droplet;
+ glUseProgram(shader->program);
+
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(texture->target, texture->tex);
+ glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ set_proj_matrix(shader->proj, pass->projection_matrix, &box);
+ glUniform1i(shader->tex, 0);
+ glUniform2f(shader->tex_size, snap->buffer->width, snap->buffer->height);
+ glUniform2f(shader->position, box.x, box.y);
+ glUniform2f(shader->size, box.width, box.height);
+ glUniform1f(shader->attach_r, options->attach_r);
+ glUniform1f(shader->sheet_r, options->sheet_r);
+ glUniform1f(shader->bow_rise, options->bow_rise);
+ glUniform1f(shader->blend_k, options->blend_k);
+ glUniform1f(shader->curve, options->curve);
+ glUniform1f(shader->band_px, options->band_px);
+ glUniform1f(shader->refr, options->refr);
+ glUniform1f(shader->ghost, options->ghost);
+
+ render(&box, &clip_region, shader->pos_attrib);
+
+ glBindTexture(texture->target, 0);
+ pixman_region32_fini(&clip_region);
+ wlr_texture_destroy(wlr_texture);
+
+ pop_fx_debug(renderer);
+}
+
void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
struct fx_render_blur_pass_options *fx_options) {
if (pass->fx_offscreen_buffers == NULL) {
diff --git a/scenefx/render/fx_renderer/fx_renderer.c b/scenefx/render/fx_renderer/fx_renderer.c
index 8d9322f..fed750a 100644
--- a/scenefx/render/fx_renderer/fx_renderer.c
+++ b/scenefx/render/fx_renderer/fx_renderer.c
@@ -96,6 +96,7 @@ static inline void free_shaders(struct fx_renderer *renderer) {
glDeleteProgram(renderer->shaders.tex_effects_ext.program);
glDeleteProgram(renderer->shaders.box_shadow.program);
glDeleteProgram(renderer->shaders.bevel.program);
+ glDeleteProgram(renderer->shaders.droplet.program);
glDeleteProgram(renderer->shaders.blur1.program);
glDeleteProgram(renderer->shaders.blur2.program);
glDeleteProgram(renderer->shaders.blur_effects.program);
@@ -429,6 +430,12 @@ static bool link_shaders(struct fx_renderer *renderer) {
goto error;
}
+ // droplet backdrop-refraction shader
+ if (!link_droplet_program(&renderer->shaders.droplet)) {
+ wlr_log(WLR_ERROR, "Could not link droplet shader");
+ goto error;
+ }
+
// Blur shaders
if (!link_blur1_program(&renderer->shaders.blur1)) {
wlr_log(WLR_ERROR, "Could not link blur1 shader");
diff --git a/scenefx/render/fx_renderer/shaders.c b/scenefx/render/fx_renderer/shaders.c
index 3903b6b..a4a83b0 100644
--- a/scenefx/render/fx_renderer/shaders.c
+++ b/scenefx/render/fx_renderer/shaders.c
@@ -19,6 +19,7 @@
#include "tex_frag_src.h"
#include "box_shadow_frag_src.h"
#include "bevel_frag_src.h"
+#include "droplet_frag_src.h"
#include "blur1_frag_src.h"
#include "blur2_frag_src.h"
#include "blur_effects_frag_src.h"
@@ -371,6 +372,30 @@ bool link_bevel_program(struct bevel_shader *shader) {
return true;
}
+bool link_droplet_program(struct droplet_shader *shader) {
+ GLuint prog;
+ shader->program = prog = link_program(droplet_frag_src);
+ if (!shader->program) {
+ return false;
+ }
+ shader->proj = glGetUniformLocation(prog, "proj");
+ shader->pos_attrib = glGetAttribLocation(prog, "pos");
+ shader->tex = glGetUniformLocation(prog, "tex");
+ shader->tex_size = glGetUniformLocation(prog, "tex_size");
+ shader->position = glGetUniformLocation(prog, "position");
+ shader->size = glGetUniformLocation(prog, "size");
+ shader->attach_r = glGetUniformLocation(prog, "attach_r");
+ shader->sheet_r = glGetUniformLocation(prog, "sheet_r");
+ shader->bow_rise = glGetUniformLocation(prog, "bow_rise");
+ shader->blend_k = glGetUniformLocation(prog, "blend_k");
+ shader->curve = glGetUniformLocation(prog, "curve");
+ shader->band_px = glGetUniformLocation(prog, "band_px");
+ shader->refr = glGetUniformLocation(prog, "refr");
+ shader->ghost = glGetUniformLocation(prog, "ghost");
+
+ return true;
+}
+
bool link_blur1_program(struct blur_shader *shader) {
GLuint prog;
shader->program = prog = link_program(blur1_frag_src);
diff --git a/scenefx/render/fx_renderer/shaders/droplet.frag b/scenefx/render/fx_renderer/shaders/droplet.frag
new file mode 100644
index 0000000..0395d28
--- /dev/null
+++ b/scenefx/render/fx_renderer/shaders/droplet.frag
@@ -0,0 +1,114 @@
+// Droplet backdrop refraction: the region behind a status-bar water drop,
+// re-rendered through the drop's lens.
+//
+// The silhouette is the same shape the cce-ui client draws — a superellipse-
+// rounded box (attach radius on the top corners, sheet radius on the bottom)
+// smooth-intersected with a huge disc whose lowest point touches the box
+// bottom (the continuous bottom arc) — so the refracting region and the
+// drawn drop can never disagree. Inside it, the backdrop texture (scenefx's
+// unblurred optimized-blur snapshot of everything beneath the bar layer) is
+// sampled with two warps:
+//
+// - a rim-weighted offset along the SDF gradient: the image bends around
+// the drop edge the way glass pulls the background with it;
+// - a faint inverted, minified sample about the drop center: the upside-
+// down image of the scene a real hanging drop shows in its belly.
+//
+// The client's translucent tint, gleam and text composite on top of this.
+
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+
+varying vec4 v_color;
+varying vec2 v_texcoord;
+
+uniform sampler2D tex;
+uniform vec2 tex_size;
+uniform vec2 position; // box origin (same space corner shaders use)
+uniform vec2 size; // box size, px
+uniform float attach_r; // top-corner radius px
+uniform float sheet_r; // bottom-corner radius px
+uniform float bow_rise; // bottom-arc edge rise px (0 = flat bottom run)
+uniform float blend_k; // smooth-intersection blend px
+uniform float curve; // corner-shape exponent (2 = circular)
+uniform float band_px; // dome skirt width px (the refraction falloff scale)
+uniform float refr; // rim refraction strength px (sign flips direction)
+uniform float ghost; // inverted-lens ghost strength 0-1
+
+// Signed distance to the drop silhouette at box-local p (y DOWN from the box
+// top). The rounded-box part is the same construction as the client's
+// rr_sdf_grad: quadrant-selected corner radius, Lp corners with a first-order
+// |grad| correction above exponent 2.
+float drop_dist(vec2 p) {
+ vec2 half_ext = size * 0.5;
+ vec2 c = p - half_ext;
+ float r = (c.y > 0.0) ? sheet_r : attach_r;
+ vec2 q = abs(c) - half_ext + vec2(r, r);
+ float d;
+ if (q.x > 0.0 && q.y > 0.0) {
+ if (curve > 2.001 && r > 0.0) {
+ vec2 u = q / r;
+ float lp = max(pow(pow(u.x, curve) + pow(u.y, curve), 1.0 / curve), 1e-4);
+ vec2 g = vec2(pow(u.x / lp, curve - 1.0), pow(u.y / lp, curve - 1.0));
+ d = r * (lp - 1.0) / max(length(g), 1e-4);
+ } else {
+ d = length(q) - r;
+ }
+ } else {
+ d = max(q.x, q.y) - r;
+ }
+ if (bow_rise > 0.25) {
+ // Smooth-intersect with the bottom-arc disc (smax = -smin(-a,-b)).
+ float hx = half_ext.x;
+ float R = hx * hx / (2.0 * bow_rise);
+ vec2 cc = vec2(hx, size.y - R);
+ float dc = length(p - cc) - R;
+ float k = max(blend_k, 1.0);
+ float hm = clamp(0.5 + 0.5 * (d - dc) / k, 0.0, 1.0);
+ d = mix(dc, d, hm) + k * hm * (1.0 - hm);
+ }
+ return d;
+}
+
+void main() {
+ // Box-local coords with y down, mirroring the corner shaders' transform.
+ vec2 rel = gl_FragCoord.xy - position;
+ rel.y = size.y - rel.y;
+
+ float d = drop_dist(rel);
+ float aa = clamp(-d + 0.5, 0.0, 1.0);
+ if (aa <= 0.0) {
+ discard;
+ }
+
+ // SDF gradient by central differences, in the same y-down local space.
+ vec2 grad = normalize(vec2(
+ drop_dist(rel + vec2(1.0, 0.0)) - drop_dist(rel - vec2(1.0, 0.0)),
+ drop_dist(rel + vec2(0.0, 1.0)) - drop_dist(rel - vec2(0.0, 1.0))
+ ) + vec2(1e-6, 0.0));
+
+ // Rim-weighted lens profile: 1 at the silhouette, 0 deep inside.
+ float t = clamp(-d / max(band_px, 1.0), 0.0, 1.0);
+ float lens = (1.0 - t) * (1.0 - t);
+
+ // Refraction: offset the sample along the gradient (scene y-down offsets
+ // negate y into gl_FragCoord space). Positive refr samples outward — the
+ // background bends around the drop edge.
+ vec2 off = grad * (refr * lens);
+ vec2 uv = (gl_FragCoord.xy + vec2(off.x, -off.y)) / tex_size;
+ vec4 col = texture2D(tex, clamp(uv, vec2(0.0), vec2(1.0)));
+
+ // Inverted lens ghost: the belly shows a faint upside-down, minified
+ // image of the scene about the drop center.
+ if (ghost > 0.001) {
+ vec2 center_fc = position + size * 0.5;
+ vec2 ghost_uv = (center_fc + (center_fc - gl_FragCoord.xy) * 0.35) / tex_size;
+ vec4 gcol = texture2D(tex, clamp(ghost_uv, vec2(0.0), vec2(1.0)));
+ col.rgb = mix(col.rgb, gcol.rgb, ghost * t * t);
+ }
+
+ gl_FragColor = vec4(col.rgb, 1.0) * aa;
+}
diff --git a/scenefx/render/fx_renderer/shaders/meson.build b/scenefx/render/fx_renderer/shaders/meson.build
index e1fe345..28441b1 100644
--- a/scenefx/render/fx_renderer/shaders/meson.build
+++ b/scenefx/render/fx_renderer/shaders/meson.build
@@ -12,6 +12,7 @@ shaders = [
'tex.frag',
'box_shadow.frag',
'bevel.frag',
+ 'droplet.frag',
'blur1.frag',
'blur2.frag',
'blur_effects.frag',
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index 97d5a40..a0c4d09 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -88,6 +88,12 @@ struct wlr_scene_bevel *wlr_scene_bevel_from_node(struct wlr_scene_node *node) {
return bevel;
}
+struct wlr_scene_droplet *wlr_scene_droplet_from_node(struct wlr_scene_node *node) {
+ assert(node->type == WLR_SCENE_NODE_DROPLET);
+ struct wlr_scene_droplet *droplet = wl_container_of(node, droplet, node);
+ return droplet;
+}
+
struct wlr_scene_blur *wlr_scene_blur_from_node(struct wlr_scene_node *node) {
assert(node->type == WLR_SCENE_NODE_BLUR);
struct wlr_scene_blur *blur = wl_container_of(node, blur, node);
@@ -287,6 +293,7 @@ static bool _scene_nodes_in_box(struct wlr_scene_node *node, struct wlr_box *box
case WLR_SCENE_NODE_BUFFER:
case WLR_SCENE_NODE_SHADOW:
case WLR_SCENE_NODE_BEVEL:
+ case WLR_SCENE_NODE_DROPLET:
case WLR_SCENE_NODE_OPTIMIZED_BLUR:
case WLR_SCENE_NODE_BLUR:;
struct wlr_box node_box = { .x = lx, .y = ly };
@@ -399,9 +406,11 @@ static void scene_node_opaque_region(struct wlr_scene_node *node, int x, int y,
// TODO: test & handle case of blur sigma = 0 and color[3] = 1?
return;
} else if (node->type == WLR_SCENE_NODE_BEVEL
+ || node->type == WLR_SCENE_NODE_DROPLET
|| node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR
|| node->type == WLR_SCENE_NODE_BLUR) {
- // Always transparent
+ // Always transparent (the droplet is opaque inside its silhouette
+ // but never box-shaped, so it must not occlude as a box).
return;
}
@@ -1184,6 +1193,60 @@ void wlr_scene_bevel_set_shoulder(struct wlr_scene_bevel *bevel, float shoulder)
scene_node_update(&bevel->node, NULL);
}
+struct wlr_scene_droplet *wlr_scene_droplet_create(struct wlr_scene_tree *parent,
+ int width, int height) {
+ struct wlr_scene_droplet *droplet = calloc(1, sizeof(*droplet));
+ if (droplet == NULL) {
+ return NULL;
+ }
+ assert(parent);
+ scene_node_init(&droplet->node, WLR_SCENE_NODE_DROPLET, parent);
+
+ droplet->width = width;
+ droplet->height = height;
+ droplet->curve = 2.0f;
+
+ scene_node_update(&droplet->node, NULL);
+
+ return droplet;
+}
+
+void wlr_scene_droplet_set_size(struct wlr_scene_droplet *droplet, int width, int height) {
+ if (droplet->width == width && droplet->height == height) {
+ return;
+ }
+ droplet->width = width;
+ droplet->height = height;
+ scene_node_update(&droplet->node, NULL);
+}
+
+void wlr_scene_droplet_set_silhouette(struct wlr_scene_droplet *droplet,
+ float attach_r, float sheet_r, float bow_rise, float blend_k, float curve) {
+ if (droplet->attach_r == attach_r && droplet->sheet_r == sheet_r
+ && droplet->bow_rise == bow_rise && droplet->blend_k == blend_k
+ && droplet->curve == curve) {
+ return;
+ }
+ droplet->attach_r = attach_r;
+ droplet->sheet_r = sheet_r;
+ droplet->bow_rise = bow_rise;
+ droplet->blend_k = blend_k;
+ droplet->curve = curve;
+ scene_node_update(&droplet->node, NULL);
+}
+
+void wlr_scene_droplet_set_lens(struct wlr_scene_droplet *droplet,
+ float band_px, float refr, float ghost) {
+ if (droplet->band_px == band_px && droplet->refr == refr
+ && droplet->ghost == ghost) {
+ return;
+ }
+ droplet->band_px = band_px;
+ droplet->refr = refr;
+ droplet->ghost = ghost;
+ scene_node_update(&droplet->node, NULL);
+}
+
void wlr_scene_bevel_set_color(struct wlr_scene_bevel *bevel, const float color[static 4]) {
if (memcmp(bevel->color, color, sizeof(bevel->color)) == 0) {
return;
@@ -1958,6 +2021,11 @@ void scene_node_get_size(struct wlr_scene_node *node, int *width, int *height) {
*width = scene_bevel->width;
*height = scene_bevel->height;
break;
+ case WLR_SCENE_NODE_DROPLET:;
+ struct wlr_scene_droplet *scene_droplet = wlr_scene_droplet_from_node(node);
+ *width = scene_droplet->width;
+ *height = scene_droplet->height;
+ break;
case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
struct wlr_scene_optimized_blur *scene_blur =
wlr_scene_optimized_blur_from_node(node);
@@ -2153,6 +2221,7 @@ static bool scene_node_at_iterator(struct wlr_scene_node *node,
}
} else if (node->type == WLR_SCENE_NODE_SHADOW
|| node->type == WLR_SCENE_NODE_BEVEL
+ || node->type == WLR_SCENE_NODE_DROPLET
|| node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR
|| node->type == WLR_SCENE_NODE_BLUR) {
// Disable interaction
@@ -2459,6 +2528,23 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
};
fx_render_pass_add_bevel(fx_pass, &bevel_options);
break;
+ case WLR_SCENE_NODE_DROPLET:;
+ struct wlr_scene_droplet *scene_droplet = wlr_scene_droplet_from_node(node);
+
+ struct fx_render_droplet_options droplet_options = {
+ .box = dst_box,
+ .attach_r = scene_droplet->attach_r * data->scale,
+ .sheet_r = scene_droplet->sheet_r * data->scale,
+ .bow_rise = scene_droplet->bow_rise * data->scale,
+ .blend_k = scene_droplet->blend_k * data->scale,
+ .curve = scene_droplet->curve,
+ .band_px = scene_droplet->band_px * data->scale,
+ .refr = scene_droplet->refr * data->scale,
+ .ghost = scene_droplet->ghost,
+ .clip = &render_region,
+ };
+ fx_render_pass_add_droplet(fx_pass, &droplet_options);
+ break;
case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
struct wlr_scene_optimized_blur *scene_blur = wlr_scene_optimized_blur_from_node(node);
// Re-render the optimized blur buffer when needed. Retry rendering
diff --git a/src/server/config.rs b/src/server/config.rs
index 59fe588..a90fb58 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -113,6 +113,11 @@ pub struct Layout {
/// (`~/.config/cce/cce-status-interface/config.kdl`, `module { spacing }`)
/// overrides the shared `style { status module_spacing }`.
pub status_module_spacing: i64,
+ /// The bar's `module { droplet }` spec string when present — the water-
+ /// drop module style. The compositor drives a scenefx droplet node
+ /// (backdrop refraction) per status segment from the SAME spec the bar
+ /// draws its drops from, so the two silhouettes cannot drift.
+ pub status_droplet: Option<String>,
pub cloud_position_default: Option<[i32; 2]>,
}
@@ -221,6 +226,7 @@ impl Default for Layout {
window_backdrop_blur_ignore_transparent: true,
status_module_hide_mode_preview: 4,
status_module_spacing: 12,
+ status_droplet: None,
cloud_position_default: None,
}
}
@@ -755,6 +761,11 @@ pub struct LayoutConfig {
pub status_module_hide_mode_preview: i64,
#[serde(default = "default_status_module_spacing")]
pub status_module_spacing: i64,
+ /// The bar's `module { droplet }` spec string (presence enables the
+ /// droplet style; the compositor's per-segment backdrop-refraction node
+ /// reads the same spec the bar draws from).
+ #[serde(default)]
+ pub status_droplet: Option<String>,
}
impl Default for LayoutConfig {
@@ -781,6 +792,7 @@ impl Default for LayoutConfig {
window_backdrop_blur_ignore_transparent: default_window_backdrop_blur_ignore_transparent(),
status_module_hide_mode_preview: default_status_module_hide_mode_preview(),
status_module_spacing: default_status_module_spacing(),
+ status_droplet: None,
}
}
}
@@ -1447,6 +1459,25 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
if let Some(i) = module_i64("height") {
layout.bar_height = i;
}
+ // The droplet spec string (presence enables the style;
+ // empty = all defaults). Same either-spelling lookup.
+ let module_str = |key: &str| -> Option<String> {
+ module
+ .entries()
+ .iter()
+ .find(|e| e.name().map(|id| id.value()) == Some(key))
+ .map(|e| e.value())
+ .or_else(|| {
+ module.children().and_then(|c| {
+ c.nodes()
+ .iter()
+ .find(|n| n.name().value() == key)
+ .and_then(|n| n.entries().first().map(|e| e.value()))
+ })
+ })
+ .and_then(|v| v.as_string().map(|s| s.to_string()))
+ };
+ layout.status_droplet = module_str("droplet");
}
}
}
@@ -2275,6 +2306,7 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
state.layout.window_backdrop_blur_ignore_transparent = config.layout.window_backdrop_blur_ignore_transparent;
state.layout.status_module_hide_mode_preview = config.layout.status_module_hide_mode_preview;
state.layout.status_module_spacing = config.layout.status_module_spacing;
+ state.layout.status_droplet = config.layout.status_droplet.clone();
state.layout.cloud_position_default = config.surface.cloud_position_default;
state.layout.shadow_enabled = config.surface.shadow_enabled;
state.layout.shadow_sigma = config.surface.shadow_sigma.max(0.0) as f32;
diff --git a/src/server/window.rs b/src/server/window.rs
index ae5721c..07f1cb7 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -359,6 +359,11 @@ pub struct Window {
/// the rim overlays the client's outermost pixels. Null if creation
/// failed (the effect is then simply absent).
pub bevel: *mut ffi::wlr_scene_bevel,
+ /// scenefx droplet node: for droplet-styled status segments, the
+ /// backdrop refracted through the drop's lens. Created BEFORE the
+ /// surfaces so it draws beneath the client's translucent drop. Null if
+ /// creation failed (the effect is then simply absent).
+ pub droplet: *mut ffi::wlr_scene_droplet,
pub decorations_below: ffi::wl_list,
pub decorations_below_tree: *mut ffi::wlr_scene_tree,
pub surfaces: crate::scene::SaveableSurfaces,
@@ -547,6 +552,14 @@ impl Window {
ffi::wlr_scene_node_set_enabled(&mut (*shadow).node, false);
}
+ // Beneath the surfaces like the shadow: the refracted backdrop must
+ // render under the client's translucent drop, not over it. Synced in
+ // update_droplet; enabled only for droplet-styled status segments.
+ let droplet = ffi::wlr_scene_droplet_create(tree, 0, 0);
+ if !droplet.is_null() {
+ ffi::wlr_scene_node_set_enabled(&mut (*droplet).node, false);
+ }
+
let black_color = [0.0f32, 0.0f32, 0.0f32, 1.0f32];
let fullscreen_background = ffi::wlr_scene_rect_create(tree, 0, 0, black_color.as_ptr());
if fullscreen_background.is_null() {
@@ -623,6 +636,7 @@ impl Window {
window_background,
shadow,
bevel,
+ droplet,
decorations_below: std::mem::zeroed(),
decorations_below_tree,
surfaces,
@@ -2295,7 +2309,7 @@ impl Window {
let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
app_id.starts_with("cce-status");
let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
- let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status);
+ let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status) && !self.droplet_backdrop_on();
let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
if is_status {
ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
@@ -2393,6 +2407,7 @@ impl Window {
&& (*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_droplet(width, height);
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
@@ -2771,7 +2786,7 @@ impl Window {
let is_status = self.tiling_mode == crate::tiling::TilingMode::Status ||
app_id.starts_with("cce-status");
let is_decorated = (*self.server).wm.is_decorated_app(&app_id);
- let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status);
+ let blur_enabled = requested.blur && (self.wm_requested.ssd || is_decorated || is_status) && !self.droplet_backdrop_on();
let mut ignore_transparent = (*self.server).wm.layout.window_backdrop_blur_ignore_transparent;
if is_status {
ignore_transparent = (*self.server).wm.layout.status_backdrop_blur_ignore_transparent;
@@ -2863,6 +2878,7 @@ impl Window {
&& (*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_droplet(width, height);
}
self.scale_only_render_finish();
@@ -2991,6 +3007,75 @@ impl Window {
ffi::wlr_scene_bevel_set_color(self.bevel, layout.bevel_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
+ /// segment. Called from BOTH render paths, like update_bevel — one-path
+ /// effects freeze at the pre-gesture zoom (the shadow's old trap).
+ pub unsafe fn update_droplet(&self, width: i32, height: i32) {
+ if self.droplet.is_null() {
+ return;
+ }
+ let node = &mut (*self.droplet).node as *mut ffi::wlr_scene_node;
+ let layout = &(*self.server).wm.layout;
+ let is_status = self.tiling_mode == crate::tiling::TilingMode::Status;
+ // Only bar-strip segments: an expanded (menu) segment is taller than
+ // the bar and draws its own grown drop client-side — refracting the
+ // collapsed silhouette beneath it would be wrong.
+ let enabled = is_status
+ && layout.status_droplet.is_some()
+ && width > 0
+ && height > 0
+ && height <= layout.bar_height as i32;
+ if !enabled {
+ ffi::wlr_scene_node_set_enabled(node, false);
+ return;
+ }
+ let spec = cce_ui::scene::paint::DropletSpec::parse(
+ layout.status_droplet.as_deref().unwrap_or(""),
+ );
+ if spec.refr <= 0.0 && spec.ghost <= 0.0 {
+ ffi::wlr_scene_node_set_enabled(node, false);
+ return;
+ }
+ ffi::wlr_scene_node_set_enabled(node, true);
+
+ // Match the client's drop box: inset 1px from the surface bottom.
+ // Camera-zoom scaling like the bevel; output scale is applied by the
+ // render pass itself.
+ let w = width as f32;
+ let h = (height as f32 - 1.0).max(1.0);
+ let (sr, ar, bow) = spec.resolve_silhouette(w, h);
+ let k = (spec.blend.max(0.0) * h).max(1.0);
+ let band = (spec.band.max(0.05) * h).max(1.0);
+ let zs = self.scale as f32;
+ ffi::wlr_scene_droplet_set_size(self.droplet, width, height);
+ ffi::wlr_scene_droplet_set_silhouette(
+ self.droplet,
+ ar * zs,
+ sr * zs,
+ bow * zs,
+ k * zs,
+ spec.curve.clamp(2.0, 6.0),
+ );
+ ffi::wlr_scene_droplet_set_lens(self.droplet, band * zs, spec.refr * zs, spec.ghost.clamp(0.0, 1.0));
+ ffi::river_scene_node_set_position_if_changed(node, 0, 0);
+ }
+ /// True when this status segment's droplet backdrop node is live. The
+ /// per-window blur must yield to it: the blur pass would composite the
+ /// UNREFRACTED cached backdrop over the lens output.
+ pub unsafe fn droplet_backdrop_on(&self) -> bool {
+ if self.droplet.is_null() || self.tiling_mode != crate::tiling::TilingMode::Status {
+ return false;
+ }
+ match (*self.server).wm.layout.status_droplet.as_deref() {
+ Some(raw) => {
+ let spec = cce_ui::scene::paint::DropletSpec::parse(raw);
+ spec.refr > 0.0 || spec.ghost > 0.0
+ }
+ None => false,
+ }
+ }
+
+
/// Advance the hover fade one tick. Every zone eases toward 1.0 if it is
/// the one under the pointer and 0.0 otherwise. Returns true while any
@@ -4291,7 +4376,7 @@ impl Decoration {
ignore_transparent = (*server).wm.layout.status_backdrop_blur_ignore_transparent;
}
let is_decorated = (*server).wm.is_decorated_app(&app_id);
- let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_decorated || is_status);
+ let blur_enabled = self.rendering_requested.blur && ((*self.window).wm_requested.ssd || is_decorated || is_status) && !(*self.window).droplet_backdrop_on();
// Radius 0 preserves existing behaviour on the layer-surface path (see layer_shell.rs)
// — it never had a blur radius applied, and this fix is scoped to toplevels.
ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, blur_enabled, (*server).wm.layout.scenefx_optimized_blur, ignore_transparent, 0, 0, 0, 0, 0);