diff --git a/docs/wiki/Configuration:-Layer-Rules.md b/docs/wiki/Configuration:-Layer-Rules.md
index c68dd878..7eff7151 100644
--- a/docs/wiki/Configuration:-Layer-Rules.md
+++ b/docs/wiki/Configuration:-Layer-Rules.md
@@ -42,6 +42,18 @@ layer-rule {
noise 0.05
saturation 3
}
+
+ popups {
+ opacity 0.5
+ geometry-corner-radius 6
+
+ background-effect {
+ xray true
+ blur true
+ noise 0.05
+ saturation 3
+ }
+ }
}
```
@@ -241,3 +253,42 @@ layer-rule {
}
}
```
+
+#### `popups`
+
+Since: next release
+
+Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
+
+The properties work the same way as the corresponding layer-rule properties, except that they apply to the layer surface's pop-ups rather than to the layer surface itself.
+
+`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
+Other properties apply independently.
+
+> [!NOTE]
+> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
+>
+> Some desktop shells will emulate pop-ups by drawing something that looks like a pop-up inside a regular layer surface.
+> As far as niri is concerned, those are just layer surfaces and not pop-ups, so this block won't apply to them.
+>
+> This block also does not affect input-method pop-ups, such as Fcitx.
+
+```kdl
+// Blur the background behind Waybar popup menus.
+layer-rule {
+ match namespace="^waybar$"
+
+ popups {
+ // Match the default GTK 3 popup corner radius.
+ geometry-corner-radius 6
+ opacity 0.85
+
+ background-effect {
+ blur true
+ }
+ }
+}
+```
+
+Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the layer surface correctly sets its Wayland geometry to exclude any shadows.
+Pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
diff --git a/docs/wiki/Configuration:-Window-Rules.md b/docs/wiki/Configuration:-Window-Rules.md
index 8566470a..5c0a0b57 100644
--- a/docs/wiki/Configuration:-Window-Rules.md
+++ b/docs/wiki/Configuration:-Window-Rules.md
@@ -107,6 +107,18 @@ window-rule {
saturation 3
}
+ popups {
+ opacity 0.5
+ geometry-corner-radius 15
+
+ background-effect {
+ xray true
+ blur true
+ noise 0.05
+ saturation 3
+ }
+ }
+
min-width 100
max-width 200
min-height 300
@@ -941,6 +953,67 @@ window-rule {
}
```
+#### `popups`
+
+Since: next release
+
+Override properties for this window's pop-ups (menus and tooltips).
+
+The properties work the same way as the corresponding window-rule properties, except that they apply to the window's pop-ups rather than to the window itself.
+
+`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
+Other properties apply independently.
+
+> [!NOTE]
+> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
+>
+> Examples of things that look like pop-ups that won't work:
+>
+> - Fully emulated by the client, i.e. not a pop-up at all, the client just draws something that looks like a pop-up inside its window.
+> These are common in game engines and in web apps, e.g. the right click menu in Google Docs or in Electron apps like Discord.
+>
+> - Uses a wl-subsurface instead of an xdg-popup.
+> Common in older apps using GTK 3, notably Firefox still uses these for some menus.
+> Subsurfaces are an indivisible part of a surface and they aren't usually pop-ups, so it wouldn't make sense for niri to apply these rules to them.
+>
+> These emulated pop-ups come with other downsides: they cannot reliably extend outside their window, and if the app tries to do that, they will be clipped by rules such as `clip-to-geometry`.
+> So most modern apps will correctly use xdg-popup, which is the intended way to show pop-ups on Wayland.
+>
+> This block also does not affect input-method pop-ups, such as Fcitx.
+>
+> For pop-ups created by your desktop shell or desktop components, use the corresponding [layer rule](./Configuration:-Layer-Rules.md#popups).
+
+```kdl
+// Blur the background behind pop-up menus in Nautilus.
+window-rule {
+ match app-id="Nautilus"
+
+ popups {
+ // Matches the default libadwaita pop-up corner radius.
+ geometry-corner-radius 15
+
+ // Note: it'll look better to set background opacity
+ // through your GTK theme CSS and not here.
+ // This is just an example that makes it look obvious.
+ opacity 0.5
+
+ background-effect {
+ blur true
+ }
+ }
+}
+```
+
+Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the window correctly sets its Wayland geometry to exclude any shadows.
+For example, GTK 4 pop-ups with pointing arrows (`has-arrow=true` property) are *not* rounded rectangles—the arrow sticks out—so if you enable blur, it will also stick out of the pop-up.
+
+| Correct | Wrong |
+|-----------------------------------------------------|--------------------------------------------------------------------------------|
+| The pop-up is a rounded rectangle. Blur looks fine. | The pop-up is not a rounded rectangle. Blur extends above, where the arrow is. |
+|  |  |
+
+These pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
+
#### Size Overrides
You can amend the window's minimum and maximum size in logical pixels.
diff --git a/docs/wiki/Window-Effects.md b/docs/wiki/Window-Effects.md
index 61e8c637..7cb96639 100644
--- a/docs/wiki/Window-Effects.md
+++ b/docs/wiki/Window-Effects.md
@@ -40,6 +40,10 @@ Blur enabled via the window rule will follow the window corner radius set via [`
On the other hand, blur enabled through `ext-background-effect` will exactly follow the shape requested by the window.
If the window or layer has clientside rounded corners or other complex shape, it should set a corresponding blur shape through `ext-background-effect`, then it will get correctly shaped background blur without any manual niri configuration.
+Windows can also blur their pop-up menus using `ext-background-effect`.
+On the niri side, you can do it with a `popups` block inside [`window-rule`](./Configuration:-Window-Rules.md#popups) and [`layer-rule`](./Configuration:-Layer-Rules.md#popups).
+See those wiki pages for examples and limitations.
+
Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur.
### Xray
diff --git a/docs/wiki/img/popup-arrow.png b/docs/wiki/img/popup-arrow.png
new file mode 100644
index 00000000..27475fce
--- /dev/null
+++ b/docs/wiki/img/popup-arrow.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5a63ea3cc2f158e175c00dd058988a2bbf676e2a2aac5c2ef1603bd983589d5
+size 166777
diff --git a/docs/wiki/img/popup-no-arrow.png b/docs/wiki/img/popup-no-arrow.png
new file mode 100644
index 00000000..396062ba
--- /dev/null
+++ b/docs/wiki/img/popup-no-arrow.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bef0c57d617916bf6014fe08e268c8201d7f6ef682e3aea3395e76116b1d0400
+size 56936
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 63d4c1c3..c0a9bd5b 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -1864,6 +1864,13 @@ mod tests {
},
popups: PopupsRule {
opacity: None,
+ geometry_corner_radius: None,
+ background_effect: BackgroundEffectRule {
+ xray: None,
+ blur: None,
+ noise: None,
+ saturation: None,
+ },
},
},
],
@@ -1908,6 +1915,13 @@ mod tests {
},
popups: PopupsRule {
opacity: None,
+ geometry_corner_radius: None,
+ background_effect: BackgroundEffectRule {
+ xray: None,
+ blur: None,
+ noise: None,
+ saturation: None,
+ },
},
},
],
diff --git a/niri-config/src/window_rule.rs b/niri-config/src/window_rule.rs
index 8dbae4d2..f2bc2ad1 100644
--- a/niri-config/src/window_rule.rs
+++ b/niri-config/src/window_rule.rs
@@ -1,7 +1,8 @@
use niri_ipc::ColumnDisplay;
use crate::appearance::{
- BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule,
+ BackgroundEffect, BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule,
+ TabIndicatorRule,
};
use crate::layout::DefaultPresetSize;
use crate::utils::{MergeWith, RegexEq};
@@ -85,6 +86,10 @@ pub struct WindowRule {
pub struct PopupsRule {
#[knuffel(child, unwrap(argument))]
pub opacity: Option,
+ #[knuffel(child)]
+ pub geometry_corner_radius: Option,
+ #[knuffel(child, default)]
+ pub background_effect: BackgroundEffectRule,
}
/// Resolved popup-specific rules.
@@ -92,6 +97,12 @@ pub struct PopupsRule {
pub struct ResolvedPopupsRules {
/// Extra opacity to draw popups with.
pub opacity: Option,
+
+ /// Corner radius to assume the popups have.
+ pub geometry_corner_radius: Option,
+
+ /// Background effect configuration for popups.
+ pub background_effect: BackgroundEffect,
}
impl MergeWith for ResolvedPopupsRules {
@@ -99,6 +110,10 @@ impl MergeWith for ResolvedPopupsRules {
if let Some(x) = part.opacity {
self.opacity = Some(x);
}
+ if let Some(x) = part.geometry_corner_radius {
+ self.geometry_corner_radius = Some(x);
+ }
+ self.background_effect.merge_with(&part.background_effect);
}
}
diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs
index 4f834386..f52ff83b 100644
--- a/src/layer/mapped.rs
+++ b/src/layer/mapped.rs
@@ -260,6 +260,7 @@ impl MappedLayer {
mut ctx: RenderCtx,
ns: Option,
location: Point,
+ xray_pos: XrayPos,
push: &mut dyn FnMut(LayerSurfaceRenderElement),
) {
if ctx.target.should_block_out(self.rules.block_out_from) {
@@ -299,10 +300,12 @@ impl MappedLayer {
let geometry = Rectangle::new(location + offset.to_f64(), popup_geo.size.to_f64());
let surface_off = popup_geo.loc.upscale(-1).to_f64();
let surface_anim_scale = Scale::from(1.);
- let effect = niri_config::BackgroundEffect {
- xray: Some(false),
- ..Default::default()
- };
+ let mut effect = popup_rules.background_effect;
+ // Default xray to false for pop-ups since they're always on top of something.
+ if effect.xray.is_none() {
+ effect.xray = Some(false);
+ }
+ let xray_pos = xray_pos.offset(offset.to_f64());
background_effect::render_for_tile(
ctx.as_gles(),
ns,
@@ -313,10 +316,10 @@ impl MappedLayer {
surface_off,
surface_anim_scale,
self.blur_config,
- niri_config::CornerRadius::default(),
+ popup_rules.geometry_corner_radius.unwrap_or_default(),
effect,
false,
- XrayPos::default(),
+ xray_pos,
&mut |elem| push(elem.into()),
);
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 0fd8ee93..f5f52b67 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -166,9 +166,10 @@ pub trait LayoutElement {
location: Point,
scale: Scale,
alpha: f32,
+ xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
- self.render_popups(ctx.r(), location, scale, alpha, push);
+ self.render_popups(ctx.r(), location, scale, alpha, xray_pos, push);
self.render_normal(ctx.r(), location, scale, alpha, push);
}
@@ -191,9 +192,10 @@ pub trait LayoutElement {
location: Point,
scale: Scale,
alpha: f32,
+ xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
- let _ = (ctx, location, scale, alpha, push);
+ let _ = (ctx, location, scale, alpha, xray_pos, push);
}
/// Renders the background effect behind the main surface of the element.
diff --git a/src/layout/tile.rs b/src/layout/tile.rs
index 7034d6aa..a2235dea 100644
--- a/src/layout/tile.rs
+++ b/src/layout/tile.rs
@@ -1065,10 +1065,14 @@ impl Tile {
.scaled_by(1. - expanded_progress as f32);
// Popups go on top, whether it's resize or not.
- self.window
- .render_popups(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| {
- push(elem.into())
- });
+ self.window.render_popups(
+ ctx.r(),
+ window_render_loc,
+ scale,
+ win_alpha,
+ xray_pos,
+ &mut |elem| push(elem.into()),
+ );
// If we're resizing, try to render a shader, or a fallback.
let mut pushed_resize = false;
diff --git a/src/niri.rs b/src/niri.rs
index e7fb6cfd..ba901276 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -4271,17 +4271,29 @@ impl Niri {
// We use macros instead of closures to avoid borrowing issues (renderer and push() go
// into different functions).
macro_rules! push_popups_from_layer {
- ($layer:expr, $ns:expr, $backdrop:expr, $push:expr) => {{
- self.render_layer_popups(ctx.r(), $ns, &layer_map, $layer, $backdrop, $push);
+ ($layer:expr, $ns:expr, $xray_pos:expr, $backdrop:expr, $push:expr) => {{
+ self.render_layer_popups(
+ ctx.r(),
+ $ns,
+ &layer_map,
+ $layer,
+ $xray_pos,
+ $backdrop,
+ $push,
+ );
}};
($layer:expr, true) => {{
- push_popups_from_layer!($layer, None, true, &mut |elem| push(elem.into()));
+ push_popups_from_layer!($layer, None, XrayPos::default(), true, &mut |elem| push(
+ elem.into()
+ ));
}};
- ($layer:expr, $ns:expr, $push:expr) => {{
- push_popups_from_layer!($layer, $ns, false, $push);
+ ($layer:expr, $ns:expr, $xray_pos:expr, $push:expr) => {{
+ push_popups_from_layer!($layer, $ns, $xray_pos, false, $push);
}};
($layer:expr) => {{
- push_popups_from_layer!($layer, None, false, &mut |elem| push(elem.into()));
+ push_popups_from_layer!($layer, None, XrayPos::default(), false, &mut |elem| push(
+ elem.into()
+ ));
}};
}
macro_rules! push_normal_from_layer {
@@ -4359,8 +4371,9 @@ impl Niri {
for (ws, geo) in mon.workspaces_with_render_geo() {
let ns = Some(ws.id().get() as usize);
- push_popups_from_layer!(Layer::Bottom, ns, process!(geo));
- push_popups_from_layer!(Layer::Background, ns, process!(geo));
+ let xray_pos = XrayPos::new(geo.loc, zoom);
+ push_popups_from_layer!(Layer::Bottom, ns, xray_pos, process!(geo));
+ push_popups_from_layer!(Layer::Background, ns, xray_pos, process!(geo));
}
mon.render_workspaces(ctx.r(), focus_ring, &mut |elem| push(elem.into()));
@@ -4515,17 +4528,21 @@ impl Niri {
}
}
+ #[allow(clippy::too_many_arguments)]
fn render_layer_popups(
&self,
mut ctx: RenderCtx,
ns: Option,
layer_map: &LayerMap,
layer: Layer,
+ xray_pos: XrayPos,
for_backdrop: bool,
push: &mut dyn FnMut(LayerSurfaceRenderElement),
) {
for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) {
- mapped.render_popups(ctx.r(), ns, geo.loc.to_f64(), push);
+ let loc = geo.loc.to_f64();
+ let xray_pos = xray_pos.offset(loc);
+ mapped.render_popups(ctx.r(), ns, loc, xray_pos, push);
}
}
@@ -5571,6 +5588,7 @@ impl Niri {
mapped.window.geometry().loc.to_f64(),
scale,
alpha,
+ XrayPos::default(),
&mut |elem| elements.push(elem.into()),
);
diff --git a/src/window/mapped.rs b/src/window/mapped.rs
index 1fb541b4..292833f1 100644
--- a/src/window/mapped.rs
+++ b/src/window/mapped.rs
@@ -535,6 +535,7 @@ impl Mapped {
location,
scale,
1.,
+ XrayPos::default(),
&mut |elem| push(use_border(elem)),
);
}
@@ -661,6 +662,7 @@ impl LayoutElement for Mapped {
location: Point,
scale: Scale,
alpha: f32,
+ xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
if ctx.target.should_block_out(self.rules.block_out_from) {
@@ -693,10 +695,12 @@ impl LayoutElement for Mapped {
let geometry = Rectangle::new(location + offset.to_f64(), popup_geo.size.to_f64());
let surface_off = popup_geo.loc.upscale(-1).to_f64();
let surface_anim_scale = Scale::from(1.);
- let effect = niri_config::BackgroundEffect {
- xray: Some(false),
- ..Default::default()
- };
+ let mut effect = popup_rules.background_effect;
+ // Default xray to false for pop-ups since they're always on top of something.
+ if effect.xray.is_none() {
+ effect.xray = Some(false);
+ }
+ let xray_pos = xray_pos.offset(offset.to_f64());
background_effect::render_for_tile(
ctx.as_gles(),
None,
@@ -707,10 +711,10 @@ impl LayoutElement for Mapped {
surface_off,
surface_anim_scale,
self.blur_config,
- CornerRadius::default(),
+ popup_rules.geometry_corner_radius.unwrap_or_default(),
effect,
false,
- XrayPos::default(),
+ xray_pos,
&mut |elem| push(elem.into()),
);
}