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. | +| ![](./img/popup-no-arrow.png) | ![](./img/popup-arrow.png) | + +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()), ); }