From f5abfe5bc61cc96a9bef72a17df847224105809a Mon Sep 17 00:00:00 2001 From: HuntedByTheIRS Date: Fri, 3 Jul 2026 02:34:29 -0400 Subject: [PATCH] Add transparency + Blur --- example/config.lua | 24 ++++++ include/client.h | 1 + parser/parser.c | 67 +++++++++++++++ parser/parser.h | 29 ++++++- src/peachwm.c | 204 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+), 2 deletions(-) diff --git a/example/config.lua b/example/config.lua index e95a895..cceeadf 100644 --- a/example/config.lua +++ b/example/config.lua @@ -75,6 +75,30 @@ effects = { only_floating = false, shadow_clip = true, }, + + transparency = { + enabled = true, + + opacity_fullscreen = 1.0, + opacity_focused = 0.98, + opacity_unfocused = 0.96, + + nogaps_transparent = true, + only_floating = false, + fullscreen_transparent = false, + + blur = { + enabled = true, + radius = 15, + passes = 3, + noise = 0.05, + + nogaps_blur = true, + only_floating = false, + fullscreen_blur = false, + blur_always = true, + }, + }, }, } diff --git a/include/client.h b/include/client.h index bad713a..9902152 100644 --- a/include/client.h +++ b/include/client.h @@ -36,6 +36,7 @@ typedef struct Client { unsigned int bw; int corner_radius; struct wlr_scene_shadow *shadow; + struct wlr_scene_blur *blur; /* WARM — accessed less frequently */ struct wl_list link; diff --git a/parser/parser.c b/parser/parser.c index 84a9095..7a41921 100644 --- a/parser/parser.c +++ b/parser/parser.c @@ -676,6 +676,71 @@ parse_shadows(lua_State *L, CfgEffectsWindowShadows *s) lua_pop(L, 1); /* pop shadows table */ } +static void +parse_blur(lua_State *L, CfgEffectsWindowBlur *b) +{ + lua_getfield(L, -1, "blur"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + b->enabled = lua_get_bool(L, "enabled", false); + b->radius = lua_get_int(L, "radius", 0); + if (b->radius < 0) b->radius = 0; + b->passes = lua_get_int(L, "passes", 0); + if (b->passes < 0) b->passes = 0; + + double n = lua_get_double(L, "noise", 0.0); + if (n < 0.0) n = 0.0; + if (n > 1.0) n = 1.0; + b->noise = (float)n; + + b->nogaps_blur = lua_get_bool(L, "nogaps_blur", false); + b->only_floating = lua_get_bool(L, "only_floating", false); + b->fullscreen_blur = lua_get_bool(L, "fullscreen_blur", false); + b->blur_always = lua_get_bool(L, "blur_always", false); + + lua_pop(L, 1); /* pop blur table */ +} + +static void +parse_transparency(lua_State *L, CfgEffectsWindowTransparency *t) +{ + lua_getfield(L, -1, "transparency"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + t->enabled = lua_get_bool(L, "enabled", false); + + double op; + + op = lua_get_double(L, "opacity_fullscreen", 1.0); + if (op < 0.0) op = 0.0; + if (op > 1.0) op = 1.0; + t->opacity_fullscreen = (float)op; + + op = lua_get_double(L, "opacity_focused", 1.0); + if (op < 0.0) op = 0.0; + if (op > 1.0) op = 1.0; + t->opacity_focused = (float)op; + + op = lua_get_double(L, "opacity_unfocused", 1.0); + if (op < 0.0) op = 0.0; + if (op > 1.0) op = 1.0; + t->opacity_unfocused = (float)op; + + t->nogaps_transparent = lua_get_bool(L, "nogaps_transparent", false); + t->only_floating = lua_get_bool(L, "only_floating", false); + t->fullscreen_transparent = lua_get_bool(L, "fullscreen_transparent", false); + + parse_blur(L, &t->blur); + + lua_pop(L, 1); /* pop transparency table */ +} + static void parse_effects(lua_State *L, CfgEffects *e) { @@ -700,6 +765,8 @@ parse_effects(lua_State *L, CfgEffects *e) parse_shadows(L, &e->windows.shadows); + parse_transparency(L, &e->windows.transparency); + lua_pop(L, 2); /* pop windows + effects */ } diff --git a/parser/parser.h b/parser/parser.h index 0d3248c..f0dbd39 100644 --- a/parser/parser.h +++ b/parser/parser.h @@ -121,9 +121,34 @@ typedef struct { bool shadow_clip; } CfgEffectsWindowShadows; +/* Window transparency */ + typedef struct { - int corner_radius; /* 0 = off, >0 = radius in pixels */ - CfgEffectsWindowShadows shadows; + bool enabled; + int radius; + int passes; + float noise; + bool nogaps_blur; + bool only_floating; + bool fullscreen_blur; + bool blur_always; +} CfgEffectsWindowBlur; + +typedef struct { + bool enabled; + float opacity_fullscreen; + float opacity_focused; + float opacity_unfocused; + bool nogaps_transparent; + bool only_floating; + bool fullscreen_transparent; + CfgEffectsWindowBlur blur; +} CfgEffectsWindowTransparency; + +typedef struct { + int corner_radius; /* 0 = off, >0 = radius in pixels */ + CfgEffectsWindowShadows shadows; + CfgEffectsWindowTransparency transparency; } CfgEffectsWindows; typedef struct { diff --git a/src/peachwm.c b/src/peachwm.c index 5011455..6f4ad1d 100644 --- a/src/peachwm.c +++ b/src/peachwm.c @@ -1756,6 +1756,14 @@ static void gpureset(struct wl_listener *listener, void *data) { wlr_compositor_set_renderer(compositor, drw); + if (cfg.effects.windows.transparency.blur.enabled) { + wlr_scene_set_blur_data(scene, + cfg.effects.windows.transparency.blur.passes, + cfg.effects.windows.transparency.blur.radius, + cfg.effects.windows.transparency.blur.noise, + 1.0f, 1.0f, 1.0f); + } + wl_list_for_each(m, &mons, link) { wlr_output_init_render(m->wlr_output, alloc, drw); } @@ -2158,6 +2166,22 @@ apply_surface_corners(struct wlr_scene_node *node, int radius) } } +static void +apply_surface_opacity(struct wlr_scene_node *node, float opacity) +{ + opacity = fmaxf(0.0f, fminf(1.0f, opacity)); + if (node->type == WLR_SCENE_NODE_BUFFER) { + struct wlr_scene_buffer *buffer = wlr_scene_buffer_from_node(node); + wlr_scene_buffer_set_opacity(buffer, opacity); + } else if (node->type == WLR_SCENE_NODE_TREE) { + struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node); + struct wlr_scene_node *child; + wl_list_for_each(child, &tree->children, link) { + apply_surface_opacity(child, opacity); + } + } +} + static void mapnotify(struct wl_listener *listener, void *data) { /* Called when the surface is mapped, or ready to display on-screen. */ Client *p = nullptr; @@ -2224,6 +2248,30 @@ static void mapnotify(struct wl_listener *listener, void *data) { : wlr_scene_subsurface_tree_create(c->scene, client_surface(c)); c->scene_surface->node.data = c; + /* Create blur node (always created, visibility controlled via set_enabled) */ + c->blur = wlr_scene_blur_create(c->scene, + (int)(c->geom.width - 2 * c->bw), + (int)(c->geom.height - 2 * c->bw)); + if (c->blur) { + wlr_scene_node_set_position(&c->blur->node, c->bw, c->bw); + /* Place blur BELOW surface so it samples background BEFORE the sharp surface composites */ + wlr_scene_node_place_below(&c->blur->node, &c->scene_surface->node); + if (c->shadow) + wlr_scene_node_place_above(&c->blur->node, &c->shadow->node); + wlr_scene_blur_set_corner_radius(c->blur, c->corner_radius); + wlr_scene_blur_set_alpha(c->blur, 1.0f); + wlr_scene_blur_set_strength(c->blur, 1.0f); + /* Set transparency mask source so SceneFX only renders blur where window has alpha < 1.0 */ + struct wlr_scene_node *child; + wl_list_for_each(child, &c->scene_surface->children, link) { + if (child->type == WLR_SCENE_NODE_BUFFER) { + wlr_scene_blur_set_transparency_mask_source(c->blur, + wlr_scene_buffer_from_node(child)); + break; + } + } + } + wlr_scene_node_raise_to_top(&c->border_bg->node); /* Initialize client geometry with room for border */ @@ -2635,6 +2683,53 @@ static void rendermon(struct wl_listener *listener, void *data) { } } + /* Apply window transparency */ + { + Client *c; + wl_list_for_each(c, &clients, link) { + if (c->mon != m) + continue; + if (!cfg.effects.windows.transparency.enabled) + continue; + if (!VISIBLEON(c, m)) + continue; + if (!c->scene_surface) + continue; + + bool transparent = true; + /* fullscreen: only if fullscreen_transparent */ + if (c->isfullscreen) { + transparent = cfg.effects.windows.transparency.fullscreen_transparent; + } else { + /* only_floating: don't apply to tiled */ + if (cfg.effects.windows.transparency.only_floating && !c->isfloating) + transparent = false; + /* nogaps_transparent: don't apply if gaps are off */ + if (!cfg.effects.windows.transparency.nogaps_transparent && !c->mon->gaps) + transparent = false; + /* smart_gaps edge case */ + if (!cfg.effects.windows.transparency.nogaps_transparent + && cfg.appearance.smart_gaps && !c->isfloating + && c->geom.x == c->mon->w.x && c->geom.y == c->mon->w.y + && c->geom.x + c->geom.width == c->mon->w.x + c->mon->w.width + && c->geom.y + c->geom.height == c->mon->w.y + c->mon->w.height) + transparent = false; + } + + float opacity = 1.0f; + if (transparent) { + if (c->isfullscreen) + opacity = cfg.effects.windows.transparency.opacity_fullscreen; + else if (c == focustop(selmon)) + opacity = cfg.effects.windows.transparency.opacity_focused; + else + opacity = cfg.effects.windows.transparency.opacity_unfocused; + } + + apply_surface_opacity(&c->scene_surface->node, opacity); + } + } + wlr_scene_output_commit(m->scene_output, nullptr); skip: @@ -2774,6 +2869,51 @@ void resize(Client *c, struct wlr_box geo, int interact) { } } + /* Blur re-apply */ + if (c->blur) { + int cw = (int)c->geom.width; + int ch = (int)c->geom.height; + int bw_i = (int)c->bw; + wlr_scene_blur_set_size(c->blur, cw - 2*bw_i, ch - 2*bw_i); + wlr_scene_node_set_position(&c->blur->node, bw_i, bw_i); + if (effective_r > 0 && !c->isfullscreen) + wlr_scene_blur_set_corner_radii(c->blur, corner_radii_all(effective_r)); + else + wlr_scene_blur_set_corner_radius(c->blur, 0); + struct clipped_region cr = { + .area = {bw_i, bw_i, cw - 2*bw_i, ch - 2*bw_i}, + .corners = effective_r > 0 ? corner_radii_all(effective_r) : corner_radii_all(0), + }; + wlr_scene_blur_set_clipped_region(c->blur, cr); + + /* Evaluate blur visibility */ + bool blur_enabled = cfg.effects.windows.transparency.blur.enabled; + if (!cfg.effects.windows.transparency.blur.nogaps_blur && !c->mon->gaps) + blur_enabled = false; + /* smart_gaps edge case */ + if (!cfg.effects.windows.transparency.blur.nogaps_blur + && cfg.appearance.smart_gaps && !c->isfloating + && c->geom.x == c->mon->w.x && c->geom.y == c->mon->w.y + && c->geom.x + c->geom.width == c->mon->w.x + c->mon->w.width + && c->geom.y + c->geom.height == c->mon->w.y + c->mon->w.height) + blur_enabled = false; + /* fullscreen */ + if (c->isfullscreen) + blur_enabled = cfg.effects.windows.transparency.blur.fullscreen_blur; + /* only_floating */ + if (!c->isfullscreen && cfg.effects.windows.transparency.blur.only_floating && !c->isfloating) + blur_enabled = false; + /* blur_always: for inherently-transparent windows */ + if (!blur_enabled && !c->isfullscreen + && cfg.effects.windows.transparency.blur.blur_always) + blur_enabled = true; + /* scratchpad windows */ + if (c->isscratchpad) + blur_enabled = false; + + wlr_scene_node_set_enabled(&c->blur->node, blur_enabled); + } + /* this is a no-op if size hasn't changed */ c->resize = client_set_size(c, c->geom.width - 2 * c->bw, c->geom.height - 2 * c->bw); @@ -2899,6 +3039,8 @@ static void setfullscreen(Client *c, int fullscreen) { if (c->border_bg) { wlr_scene_node_set_enabled(&c->border_bg->node, false); } + if (c->blur) + wlr_scene_node_set_enabled(&c->blur->node, false); } else { c->corner_radius = config_get_corner_radius(); if (c->border_bg) { @@ -3186,6 +3328,39 @@ reapply_client_appearance(void) wlr_scene_node_set_enabled(&c->shadow->node, false); } } + /* Blur re-apply on config reload */ + if (cfg.effects.windows.transparency.blur.enabled) { + if (!c->blur) { + c->blur = wlr_scene_blur_create(c->scene, + (int)(c->geom.width - 2 * (int)c->bw), + (int)(c->geom.height - 2 * (int)c->bw)); + if (c->blur) { + wlr_scene_node_set_position(&c->blur->node, c->bw, c->bw); + if (c->scene_surface) + wlr_scene_node_place_below(&c->blur->node, &c->scene_surface->node); + if (c->shadow) + wlr_scene_node_place_above(&c->blur->node, &c->shadow->node); + wlr_scene_blur_set_alpha(c->blur, 1.0f); + wlr_scene_blur_set_strength(c->blur, 1.0f); + /* Set transparency mask source */ + struct wlr_scene_node *child; + wl_list_for_each(child, &c->scene_surface->children, link) { + if (child->type == WLR_SCENE_NODE_BUFFER) { + wlr_scene_blur_set_transparency_mask_source(c->blur, + wlr_scene_buffer_from_node(child)); + break; + } + } + } + } + if (c->blur && c->corner_radius > 0 && !c->isfullscreen) + wlr_scene_blur_set_corner_radius(c->blur, c->corner_radius); + } else { + if (c->blur) { + wlr_scene_node_destroy(&c->blur->node); + c->blur = nullptr; + } + } } if (!client_is_unmanaged(c)) { float *color = c->isurgent ? cfg.appearance.urgent_color @@ -3244,6 +3419,15 @@ on_config_reload(const Config *newcfg, void *ud) nc->buttons = NULL; nc->scrolls = NULL; + /* Re-apply global blur parameters */ + if (cfg.effects.windows.transparency.blur.enabled) { + wlr_scene_set_blur_data(scene, + cfg.effects.windows.transparency.blur.passes, + cfg.effects.windows.transparency.blur.radius, + cfg.effects.windows.transparency.blur.noise, + 1.0f, 1.0f, 1.0f); + } + reapply_monitor_config(); reapply_client_appearance(); reapply_client_rules(); @@ -3274,6 +3458,15 @@ do_reload(void) fresh.buttons = NULL; fresh.scrolls = NULL; + /* Re-apply global blur parameters */ + if (cfg.effects.windows.transparency.blur.enabled) { + wlr_scene_set_blur_data(scene, + cfg.effects.windows.transparency.blur.passes, + cfg.effects.windows.transparency.blur.radius, + cfg.effects.windows.transparency.blur.noise, + 1.0f, 1.0f, 1.0f); + } + reapply_monitor_config(); reapply_client_appearance(); reapply_client_rules(); @@ -3365,6 +3558,17 @@ static void setup(void) { die("couldn't create renderer"); wl_signal_add(&drw->events.lost, &gpu_reset); + /* Set global blur parameters */ + if (cfg.effects.windows.transparency.blur.enabled) { + wlr_scene_set_blur_data(scene, + cfg.effects.windows.transparency.blur.passes, + cfg.effects.windows.transparency.blur.radius, + cfg.effects.windows.transparency.blur.noise, + 1.0f, /* brightness */ + 1.0f, /* contrast */ + 1.0f); /* saturation */ + } + /* Create shm, drm and linux_dmabuf interfaces by ourselves. * The simplest way is to call: * wlr_renderer_init_wl_display(drw);