diff --git a/Cargo.lock b/Cargo.lock
index fde5b21ffc..4dece413dd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -940,7 +940,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1068,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1416,7 +1416,7 @@ dependencies = [
"gobject-sys",
"libc",
"system-deps",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1735,7 +1735,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi 0.5.2",
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2210,6 +2210,7 @@ dependencies = [
"tracy-client",
"wayland-backend",
"wayland-client",
+ "wayland-protocols-plasma",
"wayland-scanner",
"xcursor",
"xshell",
@@ -3177,7 +3178,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -3402,7 +3403,7 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "smithay"
version = "0.7.0"
-source = "git+https://github.com/Smithay/smithay.git#ae14fa12f6ee3fc4dd4c766ffb21848f991a6a18"
+source = "git+https://github.com/YaLTeR/smithay.git?rev=5f784c9ad6ae619c880e4713e120b281e2ce5383#5f784c9ad6ae619c880e4713e120b281e2ce5383"
dependencies = [
"aliasable",
"appendlist",
@@ -3477,7 +3478,7 @@ dependencies = [
[[package]]
name = "smithay-drm-extras"
version = "0.1.0"
-source = "git+https://github.com/Smithay/smithay.git#ae14fa12f6ee3fc4dd4c766ffb21848f991a6a18"
+source = "git+https://github.com/Smithay/smithay.git#599857c6a030ac09e998078ddf1bda9f3f46db2a"
dependencies = [
"drm",
"libdisplay-info",
@@ -3583,7 +3584,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4086,6 +4087,7 @@ dependencies = [
"wayland-client",
"wayland-protocols",
"wayland-scanner",
+ "wayland-server",
]
[[package]]
@@ -4182,7 +4184,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index b19b364633..fb2040e4f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,8 +30,9 @@ tracy-client = { version = "0.18.4", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
-git = "https://github.com/Smithay/smithay.git"
+git = "https://github.com/YaLTeR/smithay.git"
# path = "../smithay"
+rev = "5f784c9ad6ae619c880e4713e120b281e2ce5383"
default-features = false
[workspace.dependencies.smithay-drm-extras]
@@ -93,6 +94,7 @@ tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
wayland-backend = "0.3.12"
+wayland-protocols-plasma = { version = "0.3.10", features = ["server"] }
wayland-scanner = "0.31.8"
xcursor = "0.3.10"
zbus = { version = "5.13.0", optional = true }
diff --git a/docs/mkdocs.yaml b/docs/mkdocs.yaml
index bfc3b364fe..1f04459b38 100644
--- a/docs/mkdocs.yaml
+++ b/docs/mkdocs.yaml
@@ -85,6 +85,7 @@ nav:
- Xwayland: Xwayland.md
- Gestures: Gestures.md
- Fullscreen and Maximize: Fullscreen-and-Maximize.md
+ - Window Effects: Window-Effects.md
- Packaging niri: Packaging-niri.md
- Integrating niri: Integrating-niri.md
- Accessibility: Accessibility.md
diff --git a/docs/wiki/Configuration:-Layer-Rules.md b/docs/wiki/Configuration:-Layer-Rules.md
index 04055bd958..c68dd87832 100644
--- a/docs/wiki/Configuration:-Layer-Rules.md
+++ b/docs/wiki/Configuration:-Layer-Rules.md
@@ -14,6 +14,7 @@ Here are all matchers and properties that a layer rule could have:
layer-rule {
match namespace="waybar"
match at-startup=true
+ match layer="top"
// Properties that apply continuously.
opacity 0.5
@@ -34,6 +35,13 @@ layer-rule {
geometry-corner-radius 12
place-within-backdrop true
baba-is-float true
+
+ background-effect {
+ xray true
+ blur true
+ noise 0.05
+ saturation 3
+ }
}
```
@@ -69,6 +77,22 @@ layer-rule {
}
```
+#### `layer`
+
+Since: next release
+
+Matches surfaces on this layer-shell layer.
+Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`.
+
+```kdl
+// Make all overlay-layer surfaces FLOAT.
+layer-rule {
+ match layer="overlay"
+
+ baba-is-float true
+}
+```
+
### Dynamic Properties
These properties apply continuously to open layer-shell surfaces.
@@ -191,3 +215,29 @@ layer-rule {
baba-is-float true
}
```
+
+#### `background-effect`
+
+Since: next release
+
+Override the background effect options for this surface.
+
+- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
+- `blur`: set to `true` to enable blur behind this surface, or `false` to force-disable it.
+- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
+- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
+
+See the [window effects page](./Window-Effects.md) for an overview of background effects.
+
+```kdl
+// Make top and overlay layers use the regular blur (if enabled),
+// while bottom and background layers keep using the efficient xray blur.
+layer-rule {
+ match layer="top"
+ match layer="overlay"
+
+ background-effect {
+ xray false
+ }
+}
+```
diff --git a/docs/wiki/Configuration:-Miscellaneous.md b/docs/wiki/Configuration:-Miscellaneous.md
index 87f001a7d7..fb366c1508 100644
--- a/docs/wiki/Configuration:-Miscellaneous.md
+++ b/docs/wiki/Configuration:-Miscellaneous.md
@@ -54,6 +54,14 @@ hotkey-overlay {
config-notification {
disable-failed
}
+
+blur {
+ // off
+ passes 3
+ offset 3.0
+ noise 0.02
+ saturation 1.5
+}
```
### `spawn-at-startup`
@@ -320,3 +328,81 @@ config-notification {
disable-failed
}
```
+
+### `blur`
+
+Since: next release
+
+Blur configuration that affects all background blur.
+
+See the [window effects page](./Window-Effects.md) for an overview of background effects.
+
+```kdl
+blur {
+ // off
+ passes 3
+ offset 3
+ noise 0.02
+ saturation 1.5
+}
+```
+
+#### `off`
+
+By default, blur is available on request by a window or layer surface (via the `ext-background-effect` protocol).
+You can also enable it manually with the `blur true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
+
+Setting the `off` flag will disable all blur, both requested by the window, and configured in window rules.
+
+```kdl
+blur {
+ off
+}
+```
+
+#### `passes` and `offset`
+
+`passes` contols the number of downsample/upsample passes for dual kawase blur.
+More passes produce a larger, smoother blur, but cost more GPU resources.
+
+`offset` is the pixel offset multiplier for each pass.
+Offset `1` is the original dual kawase blur.
+Larger values produce a smoother blur, at no additional GPU cost.
+
+However, setting `offset` too big will produce visual artifacts.
+You will need to increase `passes` to be able to use a bigger `offset` without artifacts.
+
+When configuring blur, try increasing `offset` first (since it doesn't cause any extra GPU load) until you start getting artifacts.
+Then, if you still need smoother blur, increase `passes` by 1.
+Keep doing this until you get the desired visuals.
+
+```kdl
+blur {
+ passes 3
+ offset 3.0
+}
+```
+
+#### `noise`
+
+Amount of noise to add on top of the blur.
+
+This is helpful to reduce color banding artifacts.
+
+```kdl
+blur {
+ noise 0.02
+}
+```
+
+#### `saturation`
+
+Color saturation applied to the blurred background.
+
+Values above `1` increase saturation; values below `1` reduce it.
+
+```kdl
+blur {
+ saturation 1.5
+}
+```
diff --git a/docs/wiki/Configuration:-Window-Rules.md b/docs/wiki/Configuration:-Window-Rules.md
index 5c7dd28e65..8566470a83 100644
--- a/docs/wiki/Configuration:-Window-Rules.md
+++ b/docs/wiki/Configuration:-Window-Rules.md
@@ -100,6 +100,13 @@ window-rule {
tiled-state true
baba-is-float true
+ background-effect {
+ xray true
+ blur true
+ noise 0.05
+ saturation 3
+ }
+
min-width 100
max-width 200
min-height 300
@@ -909,6 +916,31 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509
+#### `background-effect`
+
+Since: next release
+
+Override the background effect options for this window.
+
+- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
+- `blur`: set to `true` to enable blur behind this window, or `false` to force-disable it.
+- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
+- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
+
+See the [window effects page](./Window-Effects.md) for an overview of background effects.
+
+```kdl
+// Make floating windows use the regular blur (if enabled),
+// while tiled windows keep using the efficient xray blur.
+window-rule {
+ match is-floating=true
+
+ background-effect {
+ xray false
+ }
+}
+```
+
#### 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
new file mode 100644
index 0000000000..bb94707357
--- /dev/null
+++ b/docs/wiki/Window-Effects.md
@@ -0,0 +1,67 @@
+### Overview
+
+Since: next release
+
+You can apply background effects to windows and layer-shell surfaces.
+These include blur, xray, saturation, and noise.
+They can be enabled in the `background-effect {}` section of [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rules.
+
+The window needs to be semitransparent for you to see the background effect (otherwise it's fully covered by the opaque window).
+Focus ring and border can also cover the background effect, see [this FAQ entry](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows) for how to change this.
+
+### Blur
+
+Windows and layer surfaces can request their background to be blurred via the [`ext-background-effect` protocol](https://wayland.app/protocols/ext-background-effect-v1).
+In this case, the application will usually offer some "background blur" setting that you'll need to enable in its configuration.
+
+You can also enable blur on the niri side with the `blur true` background effect window rule:
+
+```kdl
+// Enable blur behind the foot terminal.
+window-rule {
+ match app-id="^foot$"
+
+ background-effect {
+ blur true
+ }
+}
+
+// Enable blur behind the fuzzel launcher.
+layer-rule {
+ match namespace="^launcher$"
+
+ background-effect {
+ blur true
+ }
+}
+```
+
+Blur enabled via the window rule will follow the window corner radius set via [`geometry-corner-radius`](./Configuration:-Window-Rules.md#geometry-corner-radius).
+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.
+
+Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur.
+
+### Xray
+
+Xray makes the window background "see through" to your wallpaper, ignoring any other windows below.
+You can enable it with `xray true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
+
+Xray is automatically enabled by default if any other background effect (like blur) is active.
+This is because it's much more efficient: with xray active, niri only needs to blur the background once, and then can reuse this blurred version with no extra work (since the wallpaper changes very rarely).
+
+#### Non-xray effects (experimental)
+
+You can disable xray with `xray false` background effect window rule.
+This gives you the normal kind of blur where everything below a window is blurred.
+Keep in mind that non-xray blur and other non-xray effects are more expensive as niri has to recompute them any time you move the window, or the contents underneath change.
+
+Non-xray effects are currently experimental because they have some known limitations.
+
+- They disappear during window open/close animations and while dragging a tiled window.
+Fixing this requries subframe support in the Smithay rendering code.
+
+- Multiple clones of a non-xray background effect will interfere with each other and cause visual glitches.
+You can see this if you enable non-xray effects on a bottom or background layer surface, then open the [Overview](./Overview.md).
+Bottom and background layer surfaces are cloned on all workspaces that you can see in the Overview, causing interference.
+Fixing this requires support for framebuffer effect clones in the Smithay rendering code.
diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md
index 4b5c830d54..570e4204e6 100644
--- a/docs/wiki/_Sidebar.md
+++ b/docs/wiki/_Sidebar.md
@@ -14,6 +14,7 @@
* [Xwayland](./Xwayland.md)
* [Gestures](./Gestures.md)
* [Fullscreen and Maximize](./Fullscreen-and-Maximize.md)
+* [Window Effects](./Window-Effects.md)
* [Packaging niri](./Packaging-niri.md)
* [Integrating niri](./Integrating-niri.md)
* [Accessibility](./Accessibility.md)
diff --git a/niri-config/src/appearance.rs b/niri-config/src/appearance.rs
index bf600feb70..6633430dc1 100644
--- a/niri-config/src/appearance.rs
+++ b/niri-config/src/appearance.rs
@@ -1006,6 +1006,110 @@ where
}
}
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Blur {
+ pub off: bool,
+ pub passes: u8,
+ pub offset: f64,
+ pub noise: f64,
+ pub saturation: f64,
+}
+
+impl Default for Blur {
+ fn default() -> Self {
+ Self {
+ off: false,
+ // TODO: tune, reduce passes
+ passes: 3,
+ offset: 3.,
+ noise: 0.02,
+ saturation: 1.5,
+ }
+ }
+}
+
+#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
+pub struct BlurPart {
+ #[knuffel(child)]
+ pub off: bool,
+ #[knuffel(child)]
+ pub on: bool,
+ #[knuffel(child, unwrap(argument))]
+ pub passes: Option,
+ #[knuffel(child, unwrap(argument))]
+ pub offset: Option>,
+ #[knuffel(child, unwrap(argument))]
+ pub noise: Option>,
+ #[knuffel(child, unwrap(argument))]
+ pub saturation: Option>,
+}
+
+impl MergeWith for Blur {
+ fn merge_with(&mut self, part: &BlurPart) {
+ self.off |= part.off;
+ if part.on {
+ self.off = false;
+ }
+
+ merge_clone!((self, part), passes);
+ merge!((self, part), offset, noise, saturation);
+ }
+}
+
+#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
+pub struct BackgroundEffectRule {
+ #[knuffel(child, unwrap(argument))]
+ pub xray: Option,
+ #[knuffel(child, unwrap(argument))]
+ pub blur: Option,
+ #[knuffel(child, unwrap(argument))]
+ pub noise: Option>,
+ #[knuffel(child, unwrap(argument))]
+ pub saturation: Option>,
+}
+
+impl MergeWith for BackgroundEffectRule {
+ fn merge_with(&mut self, part: &Self) {
+ merge_clone_opt!((self, part), xray, blur, noise, saturation);
+ }
+}
+
+/// Resolved background effect rule.
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub struct BackgroundEffect {
+ /// Whether to render with xray effect (see through).
+ ///
+ /// - `None`: xray if any background effect is active
+ /// - `Some(false)`: no xray
+ /// - `Some(true)`: xray even if no other background effect is active
+ pub xray: Option,
+
+ /// Whether to blur the background.
+ ///
+ /// - `None`: blur when the window/layer requests it (e.g. through ext-background-effect
+ /// protocol)
+ /// - `Some(false)`: never blur
+ /// - `Some(true)`: always blur
+ pub blur: Option,
+
+ pub noise: Option,
+ pub saturation: Option,
+}
+
+impl MergeWith for BackgroundEffect {
+ fn merge_with(&mut self, part: &BackgroundEffectRule) {
+ merge_clone_opt!((self, part), xray, blur);
+
+ if let Some(x) = part.noise {
+ self.noise = Some(x.0);
+ }
+
+ if let Some(x) = part.saturation {
+ self.saturation = Some(x.0);
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, assert_snapshot};
diff --git a/niri-config/src/layer_rule.rs b/niri-config/src/layer_rule.rs
index c11edc137e..d0249358a1 100644
--- a/niri-config/src/layer_rule.rs
+++ b/niri-config/src/layer_rule.rs
@@ -1,4 +1,4 @@
-use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
+use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
use crate::utils::RegexEq;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -20,6 +20,8 @@ pub struct LayerRule {
pub place_within_backdrop: Option,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option,
+ #[knuffel(child, default)]
+ pub background_effect: BackgroundEffectRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -28,4 +30,6 @@ pub struct Match {
pub namespace: Option,
#[knuffel(property)]
pub at_startup: Option,
+ #[knuffel(property, str)]
+ pub layer: Option,
}
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index b61fe1c1a0..a12c6be36c 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -78,6 +78,7 @@ pub struct Config {
pub hotkey_overlay: HotkeyOverlay,
pub config_notification: ConfigNotification,
pub animations: Animations,
+ pub blur: Blur,
pub gestures: Gestures,
pub overview: Overview,
pub environment: Environment,
@@ -194,6 +195,7 @@ where
"hotkey-overlay" => m_merge!(hotkey_overlay),
"config-notification" => m_merge!(config_notification),
"animations" => m_merge!(animations),
+ "blur" => m_merge!(blur),
"gestures" => m_merge!(gestures),
"overview" => m_merge!(overview),
"xwayland-satellite" => m_merge!(xwayland_satellite),
@@ -1616,6 +1618,13 @@ mod tests {
},
),
},
+ blur: Blur {
+ off: false,
+ passes: 3,
+ offset: 3.0,
+ noise: 0.02,
+ saturation: 1.5,
+ },
gestures: Gestures {
dnd_edge_view_scroll: DndEdgeViewScroll {
trigger_width: 10.0,
@@ -1845,6 +1854,12 @@ mod tests {
),
scroll_factor: None,
tiled_state: None,
+ background_effect: BackgroundEffectRule {
+ xray: None,
+ blur: None,
+ noise: None,
+ saturation: None,
+ },
},
],
layer_rules: [
@@ -1859,6 +1874,7 @@ mod tests {
),
),
at_startup: None,
+ layer: None,
},
],
excludes: [],
@@ -1879,6 +1895,12 @@ mod tests {
geometry_corner_radius: None,
place_within_backdrop: None,
baba_is_float: None,
+ background_effect: BackgroundEffectRule {
+ xray: None,
+ blur: None,
+ noise: None,
+ saturation: None,
+ },
},
],
binds: Binds(
diff --git a/niri-config/src/window_rule.rs b/niri-config/src/window_rule.rs
index 0465d28fad..e6d2700cba 100644
--- a/niri-config/src/window_rule.rs
+++ b/niri-config/src/window_rule.rs
@@ -1,6 +1,8 @@
use niri_ipc::ColumnDisplay;
-use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
+use crate::appearance::{
+ BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule,
+};
use crate::layout::DefaultPresetSize;
use crate::utils::RegexEq;
use crate::FloatOrInt;
@@ -72,6 +74,8 @@ pub struct WindowRule {
pub scroll_factor: Option>,
#[knuffel(child, unwrap(argument))]
pub tiled_state: Option,
+ #[knuffel(child, default)]
+ pub background_effect: BackgroundEffectRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 508456333d..b05f9994c0 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -1868,6 +1868,20 @@ impl FromStr for Transform {
}
}
+impl FromStr for Layer {
+ type Err = &'static str;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "background" => Ok(Self::Background),
+ "bottom" => Ok(Self::Bottom),
+ "top" => Ok(Self::Top),
+ "overlay" => Ok(Self::Overlay),
+ _ => Err("invalid layer, can be \"background\", \"bottom\", \"top\" or \"overlay\""),
+ }
+ }
+}
+
impl FromStr for ModeToSet {
type Err = &'static str;
diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs
index 36ecd22008..81a936d1ed 100644
--- a/niri-visual-tests/src/cases/layout.rs
+++ b/niri-visual-tests/src/cases/layout.rs
@@ -3,7 +3,7 @@ use std::time::Duration;
use niri::animation::Clock;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options, SizingMode};
-use niri::render_helpers::RenderTarget;
+use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::{Color, OutputName, PresetSize};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -270,12 +270,15 @@ impl TestCase for Layout {
self.layout.update_render_elements(Some(&self.output));
let mut rv = Vec::new();
+ let ctx = RenderCtx {
+ renderer,
+ target: RenderTarget::Output,
+ xray: None,
+ };
self.layout
.monitor_for_output(&self.output)
.unwrap()
- .render_workspaces(renderer, RenderTarget::Output, true, &mut |elem| {
- rv.push(Box::new(elem) as _)
- });
+ .render_workspaces(ctx, true, &mut |elem| rv.push(Box::new(elem) as _));
rv
}
}
diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs
index bc29a3509b..1f86681a92 100644
--- a/niri-visual-tests/src/cases/tile.rs
+++ b/niri-visual-tests/src/cases/tile.rs
@@ -2,7 +2,7 @@ use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
-use niri::render_helpers::RenderTarget;
+use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -121,13 +121,15 @@ impl TestCase for Tile {
);
let mut rv = Vec::new();
- self.tile.render(
+ let ctx = RenderCtx {
renderer,
- location,
- true,
- RenderTarget::Output,
- &mut |elem| rv.push(Box::new(elem) as _),
- );
+ target: RenderTarget::Output,
+ xray: None,
+ };
+ self.tile
+ .render(ctx, location, location, 1., true, &mut |elem| {
+ rv.push(Box::new(elem) as _)
+ });
rv
}
}
diff --git a/niri-visual-tests/src/cases/window.rs b/niri-visual-tests/src/cases/window.rs
index b5abfa0d36..b9e141d920 100644
--- a/niri-visual-tests/src/cases/window.rs
+++ b/niri-visual-tests/src/cases/window.rs
@@ -1,5 +1,5 @@
use niri::layout::{LayoutElement, SizingMode};
-use niri::render_helpers::RenderTarget;
+use niri::render_helpers::{RenderCtx, RenderTarget};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Scale, Size};
@@ -53,14 +53,15 @@ impl TestCase for Window {
.downscale(2.);
let mut rv = Vec::new();
- self.window.render_normal(
+ let ctx = RenderCtx {
renderer,
- location,
- Scale::from(1.),
- 1.,
- RenderTarget::Output,
- &mut |elem| rv.push(Box::new(elem) as _),
- );
+ target: RenderTarget::Output,
+ xray: None,
+ };
+ self.window
+ .render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| {
+ rv.push(Box::new(elem) as _)
+ });
rv
}
}
diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs
index c08d7e8d90..efb71f833f 100644
--- a/niri-visual-tests/src/test_window.rs
+++ b/niri-visual-tests/src/test_window.rs
@@ -9,7 +9,7 @@ use niri::layout::{
use niri::render_helpers::offscreen::OffscreenData;
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
-use niri::render_helpers::RenderTarget;
+use niri::render_helpers::RenderCtx;
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::Kind;
@@ -151,11 +151,10 @@ impl LayoutElement for TestWindow {
fn render_normal(
&self,
- _renderer: &mut R,
+ _ctx: RenderCtx,
location: Point,
_scale: Scale,
alpha: f32,
- _target: RenderTarget,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
let inner = self.inner.borrow();
diff --git a/src/backend/tty.rs b/src/backend/tty.rs
index 259dde3091..c1cb419bad 100644
--- a/src/backend/tty.rs
+++ b/src/backend/tty.rs
@@ -67,7 +67,7 @@ use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::renderer::AsGlesRenderer;
-use crate::render_helpers::{resources, shaders, RenderTarget};
+use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
@@ -1839,8 +1839,12 @@ impl Tty {
};
// Render the elements.
- let mut elements =
- niri.render::(&mut renderer, output, true, RenderTarget::Output);
+ let ctx = RenderCtx {
+ renderer: &mut renderer,
+ target: RenderTarget::Output,
+ xray: None,
+ };
+ let mut elements = niri.render_to_vec(ctx, output, true);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
diff --git a/src/backend/winit.rs b/src/backend/winit.rs
index 92132bd416..cf55889485 100644
--- a/src/backend/winit.rs
+++ b/src/backend/winit.rs
@@ -20,7 +20,7 @@ use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
-use crate::render_helpers::{resources, shaders, RenderTarget};
+use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
@@ -180,12 +180,12 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
- let mut elements = niri.render::(
- self.backend.renderer(),
- output,
- true,
- RenderTarget::Output,
- );
+ let ctx = RenderCtx {
+ renderer: self.backend.renderer(),
+ target: RenderTarget::Output,
+ xray: None,
+ };
+ let mut elements = niri.render_to_vec(ctx, output, true);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
diff --git a/src/handlers/background_effect.rs b/src/handlers/background_effect.rs
new file mode 100644
index 0000000000..87e3c4feb1
--- /dev/null
+++ b/src/handlers/background_effect.rs
@@ -0,0 +1,158 @@
+use std::sync::{Arc, Mutex};
+
+use smithay::delegate_background_effect;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::utils::{Logical, Point, Rectangle, Size};
+use smithay::wayland::background_effect::{
+ self, BackgroundEffectSurfaceCachedState, ExtBackgroundEffectHandler,
+};
+use smithay::wayland::compositor::{
+ add_post_commit_hook, with_states, RegionAttributes, SurfaceData,
+};
+
+use crate::delegate_kde_blur;
+use crate::niri::State;
+use crate::protocols::kde_blur::{KdeBlurHandler, KdeBlurRegion, KdeBlurSurfaceCachedState};
+use crate::utils::region::region_to_non_overlapping_rects;
+
+/// Per-surface cache for processed blur region (non-overlapping rects).
+#[derive(Default)]
+struct CachedBlurRegionUserData(Mutex);
+
+#[derive(Default)]
+struct CachedBlurRegionInner {
+ /// Whether a region change is pending to be committed.
+ pending_dirty: bool,
+ /// Whether the region must be recomputed.
+ dirty: bool,
+ /// Whether the post-commit hook has been registered for this surface.
+ hook_registered: bool,
+ /// Cached non-overlapping rects in surface-local coordinates.
+ ///
+ /// `None` means there's no blur region.
+ rects: Option>>>,
+}
+
+/// Gets the cached blur region for a surface, lazily recomputing if dirty.
+pub fn get_cached_blur_region(states: &SurfaceData) -> Option>>> {
+ let cache = states
+ .data_map
+ .get_or_insert_threadsafe(CachedBlurRegionUserData::default);
+ let mut guard = cache.0.lock().unwrap();
+
+ if guard.dirty {
+ guard.dirty = false;
+ recompute_blur_region(states, &mut guard);
+ }
+
+ guard.rects.clone()
+}
+
+fn recompute_blur_region(states: &SurfaceData, inner: &mut CachedBlurRegionInner) {
+ let cached = &states.cached_state;
+
+ let rects = if let Some(arc) = &mut inner.rects {
+ if Arc::strong_count(arc) > 1 {
+ debug!("cloning rects due to non-unique reference");
+ }
+ arc
+ } else {
+ inner.rects.insert(Arc::new(Vec::new()))
+ };
+ let rects = Arc::make_mut(rects);
+
+ // Prefer ext-background-effect.
+ if cached.has::() {
+ let mut guard = cached.get::();
+ if let Some(region) = &guard.current().blur_region {
+ region_to_non_overlapping_rects(region, rects);
+ } else {
+ inner.rects = None;
+ }
+ return;
+ }
+
+ if cached.has::() {
+ let mut guard = cached.get::();
+ match &guard.current().blur_region {
+ Some(KdeBlurRegion::WholeSurface) => {
+ // Store a single "infinite" rect that gets naturally clipped.
+ let infinite = Rectangle::new(
+ Point::new(-i32::MAX / 2, -i32::MAX / 2),
+ Size::new(i32::MAX, i32::MAX),
+ );
+ rects.clear();
+ rects.push(infinite);
+ }
+ Some(KdeBlurRegion::Region(region)) => {
+ region_to_non_overlapping_rects(region, rects);
+ }
+ None => {
+ inner.rects = None;
+ }
+ }
+ return;
+ }
+
+ // Neither is present.
+ inner.rects = None;
+}
+
+fn mark_blur_region_pending_dirty(wl_surface: &WlSurface) {
+ let register_hook = with_states(wl_surface, |states| {
+ let cache = states
+ .data_map
+ .get_or_insert_threadsafe(CachedBlurRegionUserData::default);
+ let mut guard = cache.0.lock().unwrap();
+ guard.pending_dirty = true;
+
+ if guard.hook_registered {
+ false
+ } else {
+ guard.hook_registered = true;
+ true
+ }
+ });
+
+ if register_hook {
+ add_post_commit_hook::(wl_surface, |_state, _dh, surface| {
+ with_states(surface, |states| {
+ if let Some(cache) = states.data_map.get::() {
+ let mut guard = cache.0.lock().unwrap();
+ if guard.pending_dirty {
+ guard.pending_dirty = false;
+ guard.dirty = true;
+ }
+ } else {
+ error!("unexpected missing CachedBlurRegionUserData");
+ }
+ });
+ });
+ }
+}
+
+impl ExtBackgroundEffectHandler for State {
+ fn capabilities(&self) -> background_effect::Capability {
+ background_effect::Capability::Blur
+ }
+
+ fn set_blur_region(&mut self, wl_surface: WlSurface, _region: RegionAttributes) {
+ mark_blur_region_pending_dirty(&wl_surface);
+ }
+
+ fn unset_blur_region(&mut self, wl_surface: WlSurface) {
+ mark_blur_region_pending_dirty(&wl_surface);
+ }
+}
+delegate_background_effect!(State);
+
+impl KdeBlurHandler for State {
+ fn set_blur_region(&mut self, wl_surface: WlSurface) {
+ mark_blur_region_pending_dirty(&wl_surface);
+ }
+
+ fn unset_blur_region(&mut self, wl_surface: WlSurface) {
+ mark_blur_region_pending_dirty(&wl_surface);
+ }
+}
+delegate_kde_blur!(State);
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs
index daf342296f..d70ed5f9a2 100644
--- a/src/handlers/compositor.rs
+++ b/src/handlers/compositor.rs
@@ -486,11 +486,10 @@ impl CompositorHandler for State {
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
- if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
+ if let Some((mapped, output)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
- self.backend.with_primary_renderer(|renderer| {
- self.niri.layout.store_unmap_snapshot(renderer, &window);
- });
+ let output = output.cloned();
+ self.store_unmap_snapshot(&window, output.as_ref());
}
}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index 8d67e6a047..4a48778130 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -1,3 +1,4 @@
+pub mod background_effect;
mod compositor;
mod layer_shell;
mod xdg_shell;
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 8512f85107..1182713bf1 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -846,9 +846,7 @@ impl XdgShellHandler for State {
self.niri
.stop_casts_for_target(CastTarget::Window { id: id.get() });
- self.backend.with_primary_renderer(|renderer| {
- self.niri.layout.store_unmap_snapshot(renderer, &window);
- });
+ self.store_unmap_snapshot(&window, output.as_ref());
let transaction = Transaction::new();
let blocker = transaction.blocker();
@@ -1445,7 +1443,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
- let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
+ let Some((mapped, output)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
@@ -1547,9 +1545,8 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let window = mapped.window.clone();
if got_unmapped {
- state.backend.with_primary_renderer(|renderer| {
- state.niri.layout.store_unmap_snapshot(renderer, &window);
- });
+ let output = output.cloned();
+ state.store_unmap_snapshot(&window, output.as_ref());
} else {
if animate {
state.backend.with_primary_renderer(|renderer| {
diff --git a/src/input/pick_color_grab.rs b/src/input/pick_color_grab.rs
index 100224c991..32132358c7 100644
--- a/src/input/pick_color_grab.rs
+++ b/src/input/pick_color_grab.rs
@@ -13,7 +13,7 @@ use smithay::input::SeatHandler;
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
use crate::niri::State;
-use crate::render_helpers::{render_and_download, RenderTarget};
+use crate::render_helpers::{render_and_download, RenderCtx, RenderTarget};
pub struct PickColorGrab {
start_data: PointerGrabStartData,
@@ -49,13 +49,13 @@ impl PickColorGrab {
let pos = pos_within_output.to_physical_precise_floor(scale);
let size = Size::::from((1, 1));
- let elements = data.niri.render(
+ let ctx = RenderCtx {
renderer,
- &output,
- false,
// This is an interactive operation so we can render without blocking out.
- RenderTarget::Output,
- );
+ target: RenderTarget::Output,
+ xray: None,
+ };
+ let elements = data.niri.render_to_vec(ctx, &output, false);
let mapping = match render_and_download(
renderer,
diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs
index c59baf13f1..af37e62c1f 100644
--- a/src/layer/mapped.rs
+++ b/src/layer/mapped.rs
@@ -1,20 +1,26 @@
+use std::sync::Arc;
+
use niri_config::utils::MergeWith as _;
-use niri_config::{Config, LayerRule};
+use niri_config::{Config, CornerRadius, LayerRule};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
use smithay::desktop::{LayerSurface, PopupManager};
-use smithay::utils::{Logical, Point, Scale, Size};
+use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
+use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
+use crate::handlers::background_effect::get_cached_blur_region;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
+use crate::render_helpers::background_effect::{BackgroundEffect, BackgroundEffectElement};
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::surface::push_elements_from_surface_tree;
-use crate::render_helpers::RenderTarget;
+use crate::render_helpers::{background_effect, RenderCtx};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[derive(Debug)]
@@ -31,6 +37,9 @@ pub struct MappedLayer {
/// The shadow around the surface.
shadow: Shadow,
+ /// The background effect, like blur, behind the layer-surface.
+ background_effect: BackgroundEffect,
+
/// The view size for the layer surface's output.
view_size: Size,
@@ -46,6 +55,7 @@ niri_render_elements! {
Wayland = WaylandSurfaceRenderElement,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
+ BackgroundEffect = BackgroundEffectElement,
}
}
@@ -70,6 +80,7 @@ impl MappedLayer {
view_size,
scale,
shadow: Shadow::new(shadow_config),
+ background_effect: BackgroundEffect::new(),
clock,
}
}
@@ -80,6 +91,8 @@ impl MappedLayer {
shadow_config.on = false;
shadow_config.merge_with(&self.rules.shadow);
self.shadow.update_config(shadow_config);
+
+ self.background_effect.update_config(config.blur);
}
pub fn update_shaders(&mut self) {
@@ -103,6 +116,13 @@ impl MappedLayer {
// FIXME: is_active based on keyboard focus?
self.shadow
.update_render_elements(size, true, radius, self.scale, 1.);
+
+ let has_blur_region = self.blur_region().is_some_and(|r| !r.is_empty());
+ self.background_effect.update_render_elements(
+ radius,
+ self.rules.background_effect,
+ has_blur_region,
+ );
}
pub fn are_animations_ongoing(&self) -> bool {
@@ -157,16 +177,18 @@ impl MappedLayer {
pub fn render_normal(
&self,
- renderer: &mut R,
+ mut ctx: RenderCtx,
location: Point,
- target: RenderTarget,
+ mut pos_in_backdrop: Point,
+ zoom: f64,
push: &mut dyn FnMut(LayerSurfaceRenderElement),
) {
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
+ pos_in_backdrop += self.bob_offset().upscale(zoom);
- if target.should_block_out(self.rules.block_out_from) {
+ if ctx.target.should_block_out(self.rules.block_out_from) {
// Round to physical pixels.
let location = location.to_physical_precise_round(scale).to_logical(scale);
@@ -184,7 +206,7 @@ impl MappedLayer {
let surface = self.surface.wl_surface();
push_elements_from_surface_tree(
- renderer,
+ ctx.renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
@@ -196,21 +218,76 @@ impl MappedLayer {
let location = location.to_physical_precise_round(scale).to_logical(scale);
self.shadow
- .render(renderer, location, &mut |elem| push(elem.into()));
+ .render(ctx.renderer, location, &mut |elem| push(elem.into()));
+
+ if self.background_effect.is_visible() {
+ let area = Rectangle::new(location, self.block_out_buffer.size());
+ // Effects not requested by the surface itself are drawn to match the geometry.
+ let mut clip = true;
+
+ // FIXME: support blur regions on subsurfaces in addition to the main surface.
+ let mut subregion = None;
+ let blur_geometry = if let Some(rects) = self.blur_region() {
+ if rects.is_empty() {
+ // Surface has a set, but empty blur region.
+ None
+ } else {
+ // If the surface itself requests the effects, apply different defaults.
+ clip = false;
+
+ // Use geometry-shaped blur for blocked-out layers to avoid unintentionally
+ // leaking any surface shapes. We render those layers as geometry-shaped solid
+ // rectangles anyway.
+ if ctx.target.should_block_out(self.rules.block_out_from) {
+ clip = true;
+ Some(area)
+ } else {
+ let mut main_surface_geo = self.main_surface_geo().to_f64();
+ main_surface_geo.loc += area.loc;
+
+ subregion = Some(background_effect::EffectSubregion {
+ rects,
+ scale: Scale::from(1.),
+ offset: main_surface_geo.loc,
+ });
+
+ main_surface_geo = main_surface_geo
+ .to_physical_precise_round(self.scale)
+ .to_logical(self.scale);
+ Some(main_surface_geo)
+ }
+ }
+ } else {
+ Some(area)
+ };
+
+ if let Some(geometry) = blur_geometry {
+ pos_in_backdrop += (geometry.loc - area.loc).upscale(zoom);
+ let params = background_effect::RenderParams {
+ geometry,
+ subregion,
+ clip: clip.then_some((area, CornerRadius::default())),
+ pos_in_backdrop,
+ zoom,
+ scale: self.scale,
+ };
+ self.background_effect
+ .render(ctx.as_gles(), params, &mut |elem| push(elem.into()));
+ }
+ }
}
pub fn render_popups(
&self,
- renderer: &mut R,
+ ctx: RenderCtx,
location: Point,
- target: RenderTarget,
push: &mut dyn FnMut(LayerSurfaceRenderElement),
) {
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
- if target.should_block_out(self.rules.block_out_from) {
+ if ctx.target.should_block_out(self.rules.block_out_from) {
return;
}
@@ -223,7 +300,7 @@ impl MappedLayer {
let offset = popup_offset - popup.geometry().loc;
push_elements_from_surface_tree(
- renderer,
+ ctx.renderer,
popup.wl_surface(),
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
scale,
@@ -233,4 +310,20 @@ impl MappedLayer {
);
}
}
+
+ fn main_surface_geo(&self) -> Rectangle {
+ with_states(self.surface.wl_surface(), |states| {
+ let data = states.data_map.get::();
+ data.and_then(|d| d.lock().unwrap().view())
+ .map(|view| Rectangle {
+ loc: view.offset,
+ size: view.dst,
+ })
+ })
+ .unwrap_or_default()
+ }
+
+ fn blur_region(&self) -> Option>>> {
+ with_states(self.surface.wl_surface(), get_cached_blur_region)
+ }
}
diff --git a/src/layer/mod.rs b/src/layer/mod.rs
index b74b56088e..aedf4bf7f6 100644
--- a/src/layer/mod.rs
+++ b/src/layer/mod.rs
@@ -1,7 +1,8 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::utils::MergeWith as _;
-use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
+use niri_config::{BackgroundEffect, BlockOutFrom, CornerRadius, ShadowRule};
use smithay::desktop::LayerSurface;
+use smithay::wayland::shell::wlr_layer::Layer;
pub mod mapped;
pub use mapped::MappedLayer;
@@ -26,6 +27,9 @@ pub struct ResolvedLayerRules {
/// Whether to bob this window up and down.
pub baba_is_float: bool,
+
+ /// Background effect configuration.
+ pub background_effect: BackgroundEffect,
}
impl ResolvedLayerRules {
@@ -70,6 +74,10 @@ impl ResolvedLayerRules {
}
resolved.shadow.merge_with(&rule.shadow);
+
+ resolved
+ .background_effect
+ .merge_with(&rule.background_effect);
}
resolved
@@ -83,5 +91,17 @@ fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
}
}
+ if let Some(layer) = m.layer {
+ let surface_layer = match surface.layer() {
+ Layer::Background => niri_ipc::Layer::Background,
+ Layer::Bottom => niri_ipc::Layer::Bottom,
+ Layer::Top => niri_ipc::Layer::Top,
+ Layer::Overlay => niri_ipc::Layer::Overlay,
+ };
+ if layer != surface_layer {
+ return false;
+ }
+ }
+
true
}
diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs
index b61b6c8dfb..ca28f5982f 100644
--- a/src/layout/closing_window.rs
+++ b/src/layout/closing_window.rs
@@ -21,7 +21,7 @@ use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
-use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
+use crate::render_helpers::{render_to_encompassing_texture, RenderCtx, RenderTarget};
use crate::utils::transaction::TransactionBlocker;
#[derive(Debug)]
@@ -29,6 +29,12 @@ pub struct ClosingWindow {
/// Contents of the window.
buffer: TextureBuffer,
+ /// Contents that are not blocked out, but the background is blocked out.
+ ///
+ /// If `None` then the background doesn't have any blocked-out surfaces, and normal `buffer`
+ /// can be used instead.
+ buffer_with_blocked_out_bg: Option>,
+
/// Blocked-out contents of the window.
blocked_out_buffer: TextureBuffer,
@@ -44,6 +50,9 @@ pub struct ClosingWindow {
/// How much the texture should be offset.
buffer_offset: Point,
+ /// How much the texture with blocked-out bg should be offset.
+ buffer_with_blocked_out_bg_offset: Point,
+
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point,
@@ -121,17 +130,27 @@ impl ClosingWindow {
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
+ let (buffer_with_blocked_out_bg, buffer_with_blocked_out_bg_offset) =
+ if let Some(contents) = snapshot.contents_with_blocked_out_bg {
+ let (buffer, offset) = render_to_texture(contents)
+ .context("error rendering contents with blocked-out bg")?;
+ (Some(buffer), offset)
+ } else {
+ (None, Point::default())
+ };
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
+ buffer_with_blocked_out_bg,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
buffer_offset,
+ buffer_with_blocked_out_bg_offset,
blocked_out_buffer_offset,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
@@ -159,13 +178,17 @@ impl ClosingWindow {
pub fn render(
&self,
- renderer: &mut GlesRenderer,
+ ctx: RenderCtx,
view_rect: Rectangle,
scale: Scale,
- target: RenderTarget,
) -> ClosingWindowRenderElement {
- let (buffer, offset) = if target.should_block_out(self.block_out_from) {
+ let (buffer, offset) = if ctx.target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
+ } else if ctx.target != RenderTarget::Output && self.buffer_with_blocked_out_bg.is_some() {
+ (
+ self.buffer_with_blocked_out_bg.as_ref().unwrap(),
+ self.buffer_with_blocked_out_bg_offset,
+ )
} else {
(&self.buffer, self.buffer_offset)
};
@@ -200,7 +223,10 @@ impl ClosingWindow {
let progress = anim.value();
let clamped_progress = anim.clamped_value().clamp(0., 1.);
- if Shaders::get(renderer).program(ProgramType::Close).is_some() {
+ if Shaders::get(ctx.renderer)
+ .program(ProgramType::Close)
+ .is_some()
+ {
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
diff --git a/src/layout/floating.rs b/src/layout/floating.rs
index 3fac30f22b..6fee6ad40c 100644
--- a/src/layout/floating.rs
+++ b/src/layout/floating.rs
@@ -18,7 +18,7 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
-use crate::render_helpers::RenderTarget;
+use crate::render_helpers::RenderCtx;
use crate::utils::transaction::TransactionBlocker;
use crate::utils::{
center_preferring_top_left_in_area, clamp_preferring_top_left_in_area, ensure_min_max_size,
@@ -1055,9 +1055,10 @@ impl FloatingSpace {
pub fn render(
&self,
- renderer: &mut R,
+ mut ctx: RenderCtx,
+ pos_in_backdrop: Point,
+ zoom: f64,
view_rect: Rectangle,
- target: RenderTarget,
focus_ring: bool,
push: &mut dyn FnMut(FloatingSpaceRenderElement),
) {
@@ -1067,7 +1068,7 @@ impl FloatingSpace {
//
// FIXME: I guess this should rather preserve the stacking order when the window is closed.
for closing in self.closing_windows.iter().rev() {
- let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
+ let elem = closing.render(ctx.as_gles(), view_rect, scale);
push(elem.into());
}
@@ -1076,9 +1077,15 @@ impl FloatingSpace {
// For the active tile, draw the focus ring.
let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref();
- tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
- push(elem.into())
- });
+ let pos_in_backdrop = pos_in_backdrop + tile_pos.upscale(zoom);
+ tile.render(
+ ctx.r(),
+ tile_pos,
+ pos_in_backdrop,
+ zoom,
+ focus_ring,
+ &mut |elem| push(elem.into()),
+ );
}
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 010fe65b7c..80abbb93c4 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -34,6 +34,7 @@
use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
+use std::sync::Arc;
use std::time::Duration;
use monitor::{InsertHint, InsertPosition, InsertWorkspace, MonitorAddWindowTarget};
@@ -64,7 +65,8 @@ use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::texture::TextureBuffer;
-use crate::render_helpers::{BakedBuffer, RenderTarget};
+use crate::render_helpers::xray::Xray;
+use crate::render_helpers::{BakedBuffer, RenderCtx};
use crate::rubber_band::RubberBand;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{
@@ -97,6 +99,12 @@ const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.;
/// Opacity of interactively moved tiles targeting the scrolling layout.
const INTERACTIVE_MOVE_ALPHA: f64 = 0.75;
+/// Mutter-like resistance threshold for dragging floating windows towards working area edges.
+const FLOATING_EDGE_RESISTANCE_TOWARDS_WORKING_AREA: f64 = 32.;
+
+/// Resistance threshold for movement away from working area edges.
+const FLOATING_EDGE_RESISTANCE_AWAY_WORKING_AREA: f64 = 0.;
+
/// Amount of touchpad movement to toggle the overview.
const OVERVIEW_GESTURE_MOVEMENT: f64 = 300.;
@@ -154,41 +162,38 @@ pub trait LayoutElement {
/// location.
fn render(
&self,
- renderer: &mut R,
+ mut ctx: RenderCtx,
location: Point,
scale: Scale,
alpha: f32,
- target: RenderTarget,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
- self.render_popups(renderer, location, scale, alpha, target, push);
- self.render_normal(renderer, location, scale, alpha, target, push);
+ self.render_popups(ctx.r(), location, scale, alpha, push);
+ self.render_normal(ctx.r(), location, scale, alpha, push);
}
/// Renders the non-popup parts of the element.
fn render_normal(
&self,
- renderer: &mut R,
+ ctx: RenderCtx,
location: Point,
scale: Scale,
alpha: f32,
- target: RenderTarget,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
- let _ = (renderer, location, scale, alpha, target, push);
+ let _ = (ctx, location, scale, alpha, push);
}
/// Renders the popups of the element.
fn render_popups(
&self,
- renderer: &mut R,
+ ctx: RenderCtx,
location: Point,
scale: Scale,
alpha: f32,
- target: RenderTarget,
push: &mut dyn FnMut(LayoutElementRenderElement),
) {
- let _ = (renderer, location, scale, alpha, target, push);
+ let _ = (ctx, location, scale, alpha, push);
}
/// Requests the element to change its size.
@@ -288,6 +293,16 @@ pub trait LayoutElement {
fn cancel_interactive_resize(&mut self);
fn interactive_resize_data(&self) -> Option;
+ /// Blur region (non-overlapping rects) under the main surface of this window.
+ fn blur_region(&self) -> Option>>> {
+ None
+ }
+
+ /// Returns the geometry of this window's main surface relative to the visual geometry.
+ fn main_surface_geo(&self) -> Rectangle {
+ Rectangle::from_size(self.size())
+ }
+
fn on_commit(&mut self, serial: Serial);
}
@@ -351,6 +366,7 @@ pub struct Options {
pub animations: niri_config::Animations,
pub gestures: niri_config::Gestures,
pub overview: niri_config::Overview,
+ pub blur: niri_config::Blur,
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
@@ -550,19 +566,148 @@ impl InteractiveMoveState {
}
impl InteractiveMoveData {
- fn tile_render_location(&self, zoom: f64) -> Point {
- let scale = Scale::from(self.output.current_scale().fractional_scale());
+ fn pointer_offset_within_output(&self, zoom: f64) -> Point {
let window_size = self.tile.window_size();
let pointer_offset_within_window = Point::from((
window_size.w * self.pointer_ratio_within_window.0,
window_size.h * self.pointer_ratio_within_window.1,
));
- let pos = self.pointer_pos_within_output
- - (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset())
- .upscale(zoom);
+
+ (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset())
+ .upscale(zoom)
+ }
+
+ fn tile_render_location_for_pointer(
+ &self,
+ pointer_pos_within_output: Point,
+ zoom: f64,
+ ) -> Point {
+ let scale = Scale::from(self.output.current_scale().fractional_scale());
+ let pos = pointer_pos_within_output - self.pointer_offset_within_output(zoom);
// Round to physical pixels.
pos.to_physical_precise_round(scale).to_logical(scale)
}
+
+ fn pointer_pos_for_tile_render_location(
+ &self,
+ tile_render_location: Point,
+ zoom: f64,
+ ) -> Point {
+ tile_render_location + self.pointer_offset_within_output(zoom)
+ }
+
+ fn tile_render_location(&self, zoom: f64) -> Point {
+ self.tile_render_location_for_pointer(self.pointer_pos_within_output, zoom)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+enum ResistanceSide {
+ Left,
+ Right,
+ Top,
+ Bottom,
+}
+
+fn is_moving_towards_side(side: ResistanceSide, old_pos: f64, new_pos: f64) -> bool {
+ match side {
+ ResistanceSide::Left | ResistanceSide::Top => new_pos < old_pos,
+ ResistanceSide::Right | ResistanceSide::Bottom => new_pos > old_pos,
+ }
+}
+
+fn apply_axis_edge_resistance(
+ side: ResistanceSide,
+ old_pos: f64,
+ new_pos: f64,
+ edge_pos: f64,
+) -> f64 {
+ if old_pos == new_pos {
+ return new_pos;
+ }
+
+ let moving_towards = is_moving_towards_side(side, old_pos, new_pos);
+ let threshold = if moving_towards {
+ FLOATING_EDGE_RESISTANCE_TOWARDS_WORKING_AREA
+ } else {
+ FLOATING_EDGE_RESISTANCE_AWAY_WORKING_AREA
+ };
+ let crossed_edge = match side {
+ ResistanceSide::Left | ResistanceSide::Top => old_pos >= edge_pos && new_pos <= edge_pos,
+ ResistanceSide::Right | ResistanceSide::Bottom => {
+ old_pos <= edge_pos && new_pos >= edge_pos
+ }
+ };
+
+ if moving_towards && crossed_edge && f64::abs(edge_pos - new_pos) < threshold {
+ edge_pos
+ } else {
+ new_pos
+ }
+}
+
+fn pick_stricter_translation(a: f64, b: f64) -> f64 {
+ if f64::abs(a) < f64::abs(b) {
+ a
+ } else {
+ b
+ }
+}
+
+fn ranges_overlap_or_touch(start_a: f64, end_a: f64, start_b: f64, end_b: f64) -> bool {
+ start_a <= end_b && start_b <= end_a
+}
+
+fn apply_floating_working_area_edge_resistance(
+ old_rect: Rectangle,
+ new_rect: Rectangle,
+ working_area: Rectangle,
+) -> Point {
+ let old_left = old_rect.loc.x;
+ let old_right = old_rect.loc.x + old_rect.size.w;
+ let old_top = old_rect.loc.y;
+ let old_bottom = old_rect.loc.y + old_rect.size.h;
+
+ let mut new_left = new_rect.loc.x;
+ let mut new_right = new_rect.loc.x + new_rect.size.w;
+ let mut new_top = new_rect.loc.y;
+ let new_bottom = new_rect.loc.y + new_rect.size.h;
+
+ let area_left = working_area.loc.x;
+ let area_right = working_area.loc.x + working_area.size.w;
+ let area_top = working_area.loc.y;
+ let area_bottom = working_area.loc.y + working_area.size.h;
+
+ let old_vert_overlaps_area =
+ ranges_overlap_or_touch(old_top, old_bottom, area_top, area_bottom);
+ let new_vert_overlaps_area =
+ ranges_overlap_or_touch(new_top, new_bottom, area_top, area_bottom);
+ if old_vert_overlaps_area || new_vert_overlaps_area {
+ let resisted_left =
+ apply_axis_edge_resistance(ResistanceSide::Left, old_left, new_left, area_left);
+ let resisted_right =
+ apply_axis_edge_resistance(ResistanceSide::Right, old_right, new_right, area_right);
+ let x_change =
+ pick_stricter_translation(resisted_left - old_left, resisted_right - old_right);
+ new_left = old_left + x_change;
+ new_right = old_right + x_change;
+ }
+
+ let old_horiz_overlaps_area =
+ ranges_overlap_or_touch(old_left, old_right, area_left, area_right);
+ let new_horiz_overlaps_area =
+ ranges_overlap_or_touch(new_left, new_right, area_left, area_right);
+ if old_horiz_overlaps_area || new_horiz_overlaps_area {
+ let resisted_top =
+ apply_axis_edge_resistance(ResistanceSide::Top, old_top, new_top, area_top);
+ let resisted_bottom =
+ apply_axis_edge_resistance(ResistanceSide::Bottom, old_bottom, new_bottom, area_bottom);
+ let y_change =
+ pick_stricter_translation(resisted_top - old_top, resisted_bottom - old_bottom);
+ new_top = old_top + y_change;
+ }
+
+ Point::from((new_left, new_top))
}
impl ActivateWindow {
@@ -611,6 +756,7 @@ impl Options {
animations: config.animations.clone(),
gestures: config.gestures,
overview: config.overview,
+ blur: config.blur,
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
deactivate_unfocused_windows: config.debug.deactivate_unfocused_windows,
@@ -3988,6 +4134,7 @@ impl Layout {
}
}
+ let mut output_changed = false;
if output != move_.output {
move_.tile.window().output_leave(&move_.output);
move_.tile.window().output_enter(&output);
@@ -3997,6 +4144,7 @@ impl Layout {
);
move_.output = output.clone();
self.focus_output(&output);
+ output_changed = true;
move_.output_config = self
.monitor_for_output(&output)
@@ -4015,6 +4163,46 @@ impl Layout {
move_.tile.update_config(view_size, scale, Rc::new(options));
}
+ let mut pointer_pos_within_output = pointer_pos_within_output;
+ if move_.is_floating && self.overview_progress.is_none() {
+ if let Some(mon) = self.monitor_for_output(&output) {
+ // Keep resistance anchored to the previous workspace while sticking to its
+ // edge, unless the output changed and we cannot use the previous pointer.
+ let workspace_lookup_pos = if output_changed {
+ pointer_pos_within_output
+ } else {
+ move_.pointer_pos_within_output
+ };
+
+ if let Some((ws, ws_geo)) = mon.workspace_under(workspace_lookup_pos) {
+ let zoom = mon.overview_zoom();
+ let ws_working_area = ws.working_area();
+ let working_area = Rectangle::new(
+ ws_geo.loc + ws_working_area.loc.upscale(zoom),
+ ws_working_area.size.upscale(zoom),
+ );
+
+ let old_tile_pos = move_.tile_render_location(zoom);
+ let new_tile_pos = move_
+ .tile_render_location_for_pointer(pointer_pos_within_output, zoom);
+ let tile_size = move_.tile.tile_size().upscale(zoom);
+ let old_rect = Rectangle::new(old_tile_pos, tile_size);
+ let new_rect = Rectangle::new(new_tile_pos, tile_size);
+
+ let resisted_tile_pos = apply_floating_working_area_edge_resistance(
+ old_rect,
+ new_rect,
+ working_area,
+ );
+
+ if resisted_tile_pos != new_tile_pos {
+ pointer_pos_within_output = move_
+ .pointer_pos_for_tile_render_location(resisted_tile_pos, zoom);
+ }
+ }
+ }
+ }
+
move_.pointer_pos_within_output = pointer_pos_within_output;
self.interactive_move = Some(InteractiveMoveState::Moving(move_));
@@ -4596,12 +4784,27 @@ impl Layout {
}
}
- pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
+ pub fn store_unmap_snapshot(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ xray: Option<&mut Xray>,
+ xray_has_blocked_out_layers: bool,
+ window: &W::Id,
+ ) {
let _span = tracy_client::span!("Layout::store_unmap_snapshot");
+ let zoom = self.overview_zoom();
+
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
- move_.tile.store_unmap_snapshot_if_empty(renderer);
+ let pos_in_backdrop = move_.tile_render_location(zoom);
+ move_.tile.store_unmap_snapshot_if_empty(
+ renderer,
+ xray,
+ xray_has_blocked_out_layers,
+ pos_in_backdrop,
+ zoom,
+ );
return;
}
}
@@ -4609,9 +4812,16 @@ impl Layout {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
- for ws in &mut mon.workspaces {
+ for (ws, geo) in mon.workspaces_with_render_geo_mut(false) {
if ws.has_window(window) {
- ws.store_unmap_snapshot_if_empty(renderer, window);
+ ws.store_unmap_snapshot_if_empty(
+ renderer,
+ xray,
+ xray_has_blocked_out_layers,
+ window,
+ geo.loc,
+ zoom,
+ );
return;
}
}
@@ -4620,7 +4830,14 @@ impl Layout {
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
- ws.store_unmap_snapshot_if_empty(renderer, window);
+ ws.store_unmap_snapshot_if_empty(
+ renderer,
+ xray,
+ xray_has_blocked_out_layers,
+ window,
+ Point::new(0., 0.),
+ zoom,
+ );
return;
}
}
@@ -4721,9 +4938,8 @@ impl Layout {
pub fn render_interactive_move_for_output(
&self,
- renderer: &mut R,
+ ctx: RenderCtx,
output: &Output,
- target: RenderTarget,
push: &mut dyn FnMut(RescaleRenderElement>),
) {
if self.update_render_elements_time != self.clock.now() {
@@ -4740,16 +4956,22 @@ impl Layout {
let scale = Scale::from(move_.output.current_scale().fractional_scale());
let zoom = self.overview_zoom();
- let location = move_.tile_render_location(zoom);
- move_
- .tile
- .render(renderer, location, true, target, &mut |elem| {
+ let pos_in_backdrop = move_.tile_render_location(zoom);
+
+ move_.tile.render(
+ ctx,
+ pos_in_backdrop,
+ pos_in_backdrop,
+ zoom,
+ true,
+ &mut |elem| {
push(RescaleRenderElement::from_element(
elem,
- location.to_physical_precise_round(scale),
+ pos_in_backdrop.to_physical_precise_round(scale),
zoom,
));
- });
+ },
+ );
}
pub fn refresh(&mut self, is_active: bool) {
diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs
index 420dad6706..4ed22dd977 100644
--- a/src/layout/monitor.rs
+++ b/src/layout/monitor.rs
@@ -24,7 +24,7 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::SolidColorRenderElement;
-use crate::render_helpers::RenderTarget;
+use crate::render_helpers::RenderCtx;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
use crate::utils::{
@@ -1669,8 +1669,7 @@ impl Monitor {
pub fn render_workspaces(
&self,
- renderer: &mut R,
- target: RenderTarget,
+ mut ctx: RenderCtx,
focus_ring: bool,
push: &mut dyn FnMut(MonitorRenderElement),
) {
@@ -1734,16 +1733,16 @@ impl Monitor {
}};
}
- ws.render_floating(renderer, target, focus_ring, push!());
+ ws.render_floating(ctx.r(), geo.loc, zoom, focus_ring, push!());
if let Some(loc) = insert_hint_render_loc {
if loc.workspace == InsertWorkspace::Existing(ws.id()) {
self.insert_hint_element
- .render(renderer, loc.location, push!());
+ .render(ctx.renderer, loc.location, push!());
}
}
- ws.render_scrolling(renderer, target, focus_ring, push!());
+ ws.render_scrolling(ctx.r(), geo.loc, zoom, focus_ring, push!());
}
}
diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs
index 69d0ed62a7..801f1b3f2d 100644
--- a/src/layout/scrolling.rs
+++ b/src/layout/scrolling.rs
@@ -21,7 +21,7 @@ use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
-use crate::render_helpers::RenderTarget;
+use crate::render_helpers::RenderCtx;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::ResizeEdge;
use crate::window::ResolvedWindowRules;
@@ -2899,8 +2899,9 @@ impl ScrollingSpace {
pub fn render(
&self,
- renderer: &mut R,
- target: RenderTarget,
+ mut ctx: RenderCtx,
+ pos_in_backdrop: Point,
+ zoom: f64,
focus_ring: bool,
push: &mut dyn FnMut(ScrollingSpaceRenderElement),
) {
@@ -2909,7 +2910,7 @@ impl ScrollingSpace {
// Draw the closing windows on top of the other windows.
let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size);
for closing in self.closing_windows.iter().rev() {
- let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
+ let elem = closing.render(ctx.as_gles(), view_rect, scale);
push(elem.into());
}
@@ -2930,7 +2931,7 @@ impl ScrollingSpace {
let pos = view_off + col_off + col_render_off;
let pos = pos.to_physical_precise_round(scale).to_logical(scale);
col.tab_indicator
- .render(renderer, pos, &mut |elem| push(elem.into()));
+ .render(ctx.renderer, pos, &mut |elem| push(elem.into()));
}
for (tile, tile_off, visible) in col.tiles_in_render_order() {
@@ -2955,9 +2956,15 @@ impl ScrollingSpace {
continue;
}
- tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
- push(elem.into())
- });
+ let pos_in_backdrop = pos_in_backdrop + tile_pos.upscale(zoom);
+ tile.render(
+ ctx.r(),
+ tile_pos,
+ pos_in_backdrop,
+ zoom,
+ focus_ring,
+ &mut |elem| push(elem.into()),
+ );
}
}
}
diff --git a/src/layout/tests.rs b/src/layout/tests.rs
index e22e860e5d..7af7d22f38 100644
--- a/src/layout/tests.rs
+++ b/src/layout/tests.rs
@@ -116,10 +116,12 @@ impl TestWindow {
if self.0.animate_next_configure.get() {
self.0.animation_snapshot.replace(Some(RenderSnapshot {
contents: Vec::new(),
+ contents_with_blocked_out_bg: None,
blocked_out_contents: Vec::new(),
block_out_from: None,
size: self.0.bbox.get().size.to_f64(),
texture: OnceCell::new(),
+ texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: OnceCell::new(),
}));
}
@@ -1648,6 +1650,14 @@ fn check_ops_with_options(
layout
}
+#[track_caller]
+fn moving_pointer_pos(layout: &Layout) -> Point {
+ let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move else {
+ panic!("interactive move must be in moving state");
+ };
+ move_.pointer_pos_within_output
+}
+
#[test]
fn operations_dont_panic() {
if std::env::var_os("RUN_SLOW_TESTS").is_none() {
@@ -3029,6 +3039,158 @@ fn interactive_move_from_workspace_with_layout_config() {
check_ops(ops);
}
+#[test]
+fn floating_edge_resistance_sticks_and_releases() {
+ let area = Rectangle::from_size(Size::from((1280., 720.)));
+ let size = Size::from((100., 200.));
+
+ let old = Rectangle::new(Point::from((40., 100.)), size);
+ let new = Rectangle::new(Point::from((20., 100.)), size);
+ let resisted = apply_floating_working_area_edge_resistance(old, new, area);
+ assert_eq!(resisted, Point::from((20., 100.)));
+
+ let old = Rectangle::new(Point::from((20., 100.)), size);
+ let new = Rectangle::new(Point::from((-10., 100.)), size);
+ let resisted = apply_floating_working_area_edge_resistance(old, new, area);
+ assert_eq!(resisted, Point::from((0., 100.)));
+
+ let old = Rectangle::new(Point::from((0., 100.)), size);
+ let new = Rectangle::new(Point::from((-31., 100.)), size);
+ let resisted = apply_floating_working_area_edge_resistance(old, new, area);
+ assert_eq!(resisted, Point::from((0., 100.)));
+
+ let old = Rectangle::new(Point::from((0., 100.)), size);
+ let new = Rectangle::new(Point::from((-33., 100.)), size);
+ let resisted = apply_floating_working_area_edge_resistance(old, new, area);
+ assert_eq!(resisted, Point::from((-33., 100.)));
+}
+
+#[test]
+fn floating_edge_resistance_does_not_apply_away_from_edge() {
+ let area = Rectangle::from_size(Size::from((1280., 720.)));
+ let size = Size::from((100., 200.));
+
+ let old = Rectangle::new(Point::from((10., 100.)), size);
+ let new = Rectangle::new(Point::from((20., 100.)), size);
+ let resisted = apply_floating_working_area_edge_resistance(old, new, area);
+ assert_eq!(resisted, Point::from((20., 100.)));
+}
+
+#[test]
+fn pick_stricter_translation_prefers_smaller_magnitude() {
+ assert_eq!(pick_stricter_translation(-5., -12.), -5.);
+ assert_eq!(pick_stricter_translation(7., -3.), -3.);
+ assert_eq!(pick_stricter_translation(4., -4.), -4.);
+}
+
+#[test]
+fn floating_interactive_move_resists_working_area_edges() {
+ let mut layout = Layout::default();
+ Op::AddOutput(1).apply(&mut layout);
+ Op::AddOutput(2).apply(&mut layout);
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(1)
+ },
+ }
+ .apply(&mut layout);
+
+ Op::InteractiveMoveBegin {
+ window: 1,
+ output_idx: 1,
+ px: 0.,
+ py: 0.,
+ }
+ .apply(&mut layout);
+ Op::InteractiveMoveUpdate {
+ window: 1,
+ dx: 0.,
+ dy: 0.,
+ output_idx: 2,
+ px: 40.,
+ py: 40.,
+ }
+ .apply(&mut layout);
+
+ Op::InteractiveMoveUpdate {
+ window: 1,
+ dx: 0.,
+ dy: 0.,
+ output_idx: 2,
+ px: 20.,
+ py: 20.,
+ }
+ .apply(&mut layout);
+ assert_eq!(moving_pointer_pos(&layout), Point::from((20., 20.)));
+
+ Op::InteractiveMoveUpdate {
+ window: 1,
+ dx: 0.,
+ dy: 0.,
+ output_idx: 2,
+ px: -10.,
+ py: -10.,
+ }
+ .apply(&mut layout);
+ assert_eq!(moving_pointer_pos(&layout), Point::from((0., 0.)));
+
+ Op::InteractiveMoveUpdate {
+ window: 1,
+ dx: 0.,
+ dy: 0.,
+ output_idx: 2,
+ px: -40.,
+ py: -40.,
+ }
+ .apply(&mut layout);
+ assert_eq!(moving_pointer_pos(&layout), Point::from((-40., -40.)));
+}
+
+#[test]
+fn floating_interactive_move_has_no_resistance_in_overview() {
+ let mut layout = Layout::default();
+ Op::AddOutput(1).apply(&mut layout);
+ Op::AddOutput(2).apply(&mut layout);
+ Op::AddWindow {
+ params: TestWindowParams {
+ is_floating: true,
+ ..TestWindowParams::new(1)
+ },
+ }
+ .apply(&mut layout);
+
+ Op::InteractiveMoveBegin {
+ window: 1,
+ output_idx: 1,
+ px: 0.,
+ py: 0.,
+ }
+ .apply(&mut layout);
+ Op::InteractiveMoveUpdate {
+ window: 1,
+ dx: 0.,
+ dy: 0.,
+ output_idx: 2,
+ px: 40.,
+ py: 0.,
+ }
+ .apply(&mut layout);
+
+ layout.toggle_overview();
+
+ Op::InteractiveMoveUpdate {
+ window: 1,
+ dx: 0.,
+ dy: 0.,
+ output_idx: 2,
+ px: 20.,
+ py: 0.,
+ }
+ .apply(&mut layout);
+ assert_eq!(moving_pointer_pos(&layout), Point::from((20., 0.)));
+}
+
#[test]
fn set_width_fixed_negative() {
let ops = [
diff --git a/src/layout/tile.rs b/src/layout/tile.rs
index bb0d61e873..3db767729a 100644
--- a/src/layout/tile.rs
+++ b/src/layout/tile.rs
@@ -18,6 +18,7 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::layout::SizingMode;
use crate::niri_render_elements;
+use crate::render_helpers::background_effect::{self, BackgroundEffect, BackgroundEffectElement};
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
@@ -27,7 +28,8 @@ use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
-use crate::render_helpers::RenderTarget;
+use crate::render_helpers::xray::Xray;
+use crate::render_helpers::{RenderCtx, RenderTarget};
use crate::utils::transaction::Transaction;
use crate::utils::{
baba_is_float_offset, round_logical_in_physical, round_logical_in_physical_max1,
@@ -57,6 +59,9 @@ pub struct Tile {
/// The black backdrop for fullscreen windows.
fullscreen_backdrop: SolidColorBuffer,
+ /// The background effect, like blur, behind the window.
+ background_effect: BackgroundEffect,
+
/// Whether the tile should float upon unfullscreening.
pub(super) restore_to_floating: bool,
@@ -130,6 +135,7 @@ niri_render_elements! {
ClippedSurface = ClippedSurfaceRenderElement,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
+ BackgroundEffect = BackgroundEffectElement,
}
}
@@ -192,6 +198,7 @@ impl Tile {
shadow: Shadow::new(shadow_config),
sizing_mode,
fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
+ background_effect: BackgroundEffect::new(),
restore_to_floating: false,
floating_window_size: None,
floating_pos: None,
@@ -248,6 +255,8 @@ impl Tile {
let shadow_config = self.options.layout.shadow.merged_with(&rules.shadow);
self.shadow.update_config(shadow_config);
+
+ self.background_effect.update_config(self.options.blur);
}
pub fn update_shaders(&mut self) {
@@ -403,7 +412,6 @@ impl Tile {
.unwrap_or_default()
.fit_to(window_size.w as f32, window_size.h as f32);
self.rounded_corner_damage.set_corner_radius(radius);
- self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self) {
@@ -457,6 +465,17 @@ impl Tile {
let animated_tile_size = self.animated_tile_size();
let expanded_progress = self.expanded_progress();
+ let radius = rules
+ .geometry_corner_radius
+ .unwrap_or_default()
+ .scaled_by(1. - expanded_progress as f32);
+ let has_blur_region = self.window.blur_region().is_some_and(|r| !r.is_empty());
+ self.background_effect.update_render_elements(
+ radius,
+ rules.background_effect,
+ has_blur_region,
+ );
+
let draw_border_with_background = rules
.draw_border_with_background
.unwrap_or_else(|| !self.window.has_ssd());
@@ -1009,10 +1028,11 @@ impl Tile {
fn render_inner(
&self,
- renderer: &mut R,
+ mut ctx: RenderCtx,
location: Point,
+ mut pos_in_backdrop: Point,
+ zoom: f64,
focus_ring: bool,
- target: RenderTarget,
push: &mut dyn FnMut(TileRenderElement),
) {
let _span = tracy_client::span!("Tile::render_inner");
@@ -1040,11 +1060,13 @@ impl Tile {
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
// passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset();
+ pos_in_backdrop += self.bob_offset().upscale(zoom);
let window_loc = self.window_loc();
let window_size = self.window_size();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
+ pos_in_backdrop += window_loc.upscale(zoom);
let area = Rectangle::new(window_render_loc, animated_window_size);
let rules = self.window.rules();
@@ -1058,48 +1080,43 @@ impl Tile {
.scaled_by(1. - expanded_progress as f32);
// Popups go on top, whether it's resize or not.
- self.window.render_popups(
- renderer,
- window_render_loc,
- scale,
- win_alpha,
- target,
- &mut |elem| push(elem.into()),
- );
+ self.window
+ .render_popups(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| {
+ push(elem.into())
+ });
// If we're resizing, try to render a shader, or a fallback.
let mut pushed_resize = false;
if let Some(resize) = &self.resize_animation {
- if ResizeRenderElement::has_shader(renderer) {
- let gles_renderer = renderer.as_gles_renderer();
+ if ResizeRenderElement::has_shader(ctx.renderer) {
+ let mut ctx = ctx.as_gles();
- if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
+ if let Some(texture_from) = resize.snapshot.texture(ctx.r(), scale) {
let mut window_elements = Vec::new();
self.window.render_normal(
- gles_renderer,
+ ctx.r(),
Point::from((0., 0.)),
scale,
1.,
- target,
&mut |elem| window_elements.push(elem),
);
let current = resize
.offscreen
- .render(gles_renderer, scale, &window_elements)
+ .render(ctx.renderer, scale, &window_elements)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
// Clip blocked-out resizes unconditionally because they use solid color render
// elements.
- let clip_to_geometry = if target
- .should_block_out(resize.snapshot.block_out_from)
- && target.should_block_out(rules.block_out_from)
- {
- true
- } else {
- clip_to_geometry
- };
+ let clip_to_geometry =
+ if ctx.target.should_block_out(resize.snapshot.block_out_from)
+ && ctx.target.should_block_out(rules.block_out_from)
+ {
+ true
+ } else {
+ clip_to_geometry
+ };
if let Some((elem_current, _sync_point, mut data)) = current {
let texture_current = elem_current.texture().clone();
@@ -1148,12 +1165,12 @@ impl Tile {
}
// If we're not resizing, render the window itself.
- let has_border_shader = BorderRenderElement::has_shader(renderer);
+ let has_border_shader = BorderRenderElement::has_shader(ctx.renderer);
if !pushed_resize {
let geo = Rectangle::new(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
- let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
+ let clip_shader = ClippedSurfaceRenderElement::shader(ctx.renderer).cloned();
let clip = |elem| match elem {
LayoutElementRenderElement::Wayland(elem) => {
// If we should clip to geometry, render a clipped window.
@@ -1206,18 +1223,14 @@ impl Tile {
};
if clip_to_geometry && clip_shader.is_some() {
- let damage = self.rounded_corner_damage.element();
- push(damage.with_location(window_render_loc).into());
+ let damage = self.rounded_corner_damage.render(geo);
+ push(damage.into());
}
- self.window.render_normal(
- renderer,
- window_render_loc,
- scale,
- win_alpha,
- target,
- &mut |elem| push(clip(elem)),
- );
+ self.window
+ .render_normal(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| {
+ push(clip(elem))
+ });
}
if fullscreen_progress > 0. {
@@ -1264,7 +1277,7 @@ impl Tile {
if let Some(width) = self.visual_border_width() {
self.border.render(
- renderer,
+ ctx.renderer,
location + Point::from((width, width)),
&mut |elem| push(elem.into()),
);
@@ -1276,21 +1289,79 @@ impl Tile {
// a bit weird).
if focus_ring && expanded_progress < 1. {
self.focus_ring
- .render(renderer, location, &mut |elem| push(elem.into()));
+ .render(ctx.renderer, location, &mut |elem| push(elem.into()));
}
if expanded_progress < 1. {
self.shadow
- .render(renderer, location, &mut |elem| push(elem.into()));
+ .render(ctx.renderer, location, &mut |elem| push(elem.into()));
+ }
+
+ if self.background_effect.is_visible() {
+ // Effects not requested by the surface itself are drawn to match the geometry.
+ let mut clip = true;
+
+ // FIXME: support blur regions on subsurfaces in addition to the main surface.
+ let mut subregion = None;
+ let blur_geometry = if let Some(rects) = self.window.blur_region() {
+ if rects.is_empty() {
+ // Surface has a set, but empty blur region.
+ None
+ } else {
+ // If the surface itself requests the effects, apply different defaults.
+ clip = rules.clip_to_geometry == Some(true);
+
+ // Use geometry-shaped blur for blocked-out windows to avoid unintentionally
+ // leaking any surface shapes. We render those windows as geometry-shaped solid
+ // rectangles anyway.
+ if ctx.target.should_block_out(rules.block_out_from) {
+ clip = true;
+ Some(area)
+ } else {
+ let anim_scale = animated_window_size / window_size;
+ let mut main_surface_geo =
+ self.window.main_surface_geo().to_f64().upscale(anim_scale);
+ main_surface_geo.loc += area.loc;
+
+ subregion = Some(background_effect::EffectSubregion {
+ rects,
+ scale: anim_scale,
+ offset: main_surface_geo.loc,
+ });
+
+ main_surface_geo = main_surface_geo
+ .to_physical_precise_round(self.scale)
+ .to_logical(self.scale);
+ Some(main_surface_geo)
+ }
+ }
+ } else {
+ Some(area)
+ };
+
+ if let Some(geometry) = blur_geometry {
+ pos_in_backdrop += (geometry.loc - area.loc).upscale(zoom);
+ let params = background_effect::RenderParams {
+ geometry,
+ subregion,
+ clip: clip.then_some((area, CornerRadius::default())),
+ pos_in_backdrop,
+ zoom,
+ scale: self.scale,
+ };
+ self.background_effect
+ .render(ctx.as_gles(), params, &mut |elem| push(elem.into()));
+ }
}
}
pub fn render(
&self,
- renderer: &mut R,
+ mut ctx: RenderCtx,
location: Point,
+ pos_in_backdrop: Point,
+ zoom: f64,
focus_ring: bool,
- target: RenderTarget,
push: &mut dyn FnMut(TileRenderElement),
) {
let _span = tracy_client::span!("Tile::render");
@@ -1306,17 +1377,18 @@ impl Tile {
self.window().set_offscreen_data(None);
if let Some(open) = &self.open_animation {
- let renderer = renderer.as_gles_renderer();
+ let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(
- renderer,
- Point::from((0., 0.)),
+ ctx.r(),
+ Point::new(0., 0.),
+ pos_in_backdrop,
+ zoom,
focus_ring,
- target,
&mut |elem| elements.push(elem),
);
match open.render(
- renderer,
+ ctx.renderer,
&elements,
self.animated_tile_size(),
location,
@@ -1333,16 +1405,17 @@ impl Tile {
}
}
} else if let Some(alpha) = &self.alpha_animation {
- let renderer = renderer.as_gles_renderer();
+ let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(
- renderer,
- Point::from((0., 0.)),
+ ctx.r(),
+ Point::new(0., 0.),
+ pos_in_backdrop,
+ zoom,
focus_ring,
- target,
&mut |elem| elements.push(elem),
);
- match alpha.offscreen.render(renderer, scale, &elements) {
+ match alpha.offscreen.render(ctx.renderer, scale, &elements) {
Ok((elem, _sync, data)) => {
let offset = elem.offset();
let elem = elem.with_alpha(tile_alpha).with_offset(location + offset);
@@ -1358,48 +1431,154 @@ impl Tile {
}
if !pushed {
- self.render_inner(renderer, location, focus_ring, target, &mut |elem| {
- push(elem)
- });
+ self.render_inner(
+ ctx,
+ location,
+ pos_in_backdrop,
+ zoom,
+ focus_ring,
+ &mut |elem| push(elem),
+ );
}
}
- pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
+ pub fn store_unmap_snapshot_if_empty(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ xray: Option<&mut Xray>,
+ xray_has_blocked_out_layers: bool,
+ pos_in_backdrop: Point,
+ zoom: f64,
+ ) {
if self.unmap_snapshot.is_some() {
return;
}
- self.unmap_snapshot = Some(self.render_snapshot(renderer));
+ self.unmap_snapshot = Some(self.render_snapshot(
+ renderer,
+ xray,
+ xray_has_blocked_out_layers,
+ pos_in_backdrop,
+ zoom,
+ ));
}
- fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
+ fn render_snapshot(
+ &self,
+ renderer: &mut GlesRenderer,
+ mut xray: Option<&mut Xray>,
+ xray_has_blocked_out_layers: bool,
+ pos_in_backdrop: Point,
+ zoom: f64,
+ ) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let mut contents = Vec::new();
self.render(
- renderer,
+ RenderCtx {
+ target: RenderTarget::Output,
+ renderer,
+ xray: xray.as_deref(),
+ },
Point::from((0., 0.)),
+ pos_in_backdrop,
+ zoom,
false,
- RenderTarget::Output,
&mut |elem| contents.push(elem),
);
+ let mut contents_with_blocked_out_bg = None;
+
+ // Do a bit of pointer surgery on Xray.
+ //
+ // The idea is to avoid the combinatorial combination of rendering snapshots for target
+ // (Output, Screencast) × Xray target (Output, Screencast, ScreenCapture).
+ //
+ // Our main goals:
+ // - Everything must look unblocked for RenderTarget::Output.
+ // - If anything is potentially blocked-out, it must not show up on any screen capture.
+ //
+ // Right above we rendered a fully-unblocked snapshot for the Output, so that's covered.
+ //
+ // Next, *only if Xray has any blocked-out surfaces* (which is a rare case), we will render
+ // a snapshot where the window itself is unblocked, but the Xray background is blocked. To
+ // do this, we swap the Output target buffers in Xray with the Screencast target buffers
+ // (which were prepared for us higher up the stack).
+ //
+ // Finally, we render a fully blocked-out snapshot. If Xray has blocked-out surfaces, then
+ // Xray's Screencast buffers are already filled-in, but if not, then we swap in the Output
+ // buffers, to avoid an extra render. This is safe since we know there are no blocked
+ // surfaces there.
+ let output_idx = RenderTarget::Output as usize;
+ let screencast_idx = RenderTarget::Screencast as usize;
+ let mut screencast_background = None;
+ let mut screencast_backdrop = None;
+ let mut output_background = None;
+ let mut output_backdrop = None;
+ if let Some(xray) = &mut xray {
+ screencast_background = Some(Rc::clone(&xray.background[screencast_idx]));
+ screencast_backdrop = Some(Rc::clone(&xray.backdrop[screencast_idx]));
+ output_background = Some(Rc::clone(&xray.background[output_idx]));
+ output_backdrop = Some(Rc::clone(&xray.backdrop[output_idx]));
+
+ if xray_has_blocked_out_layers {
+ xray.background[output_idx] = screencast_background.clone().unwrap();
+ xray.backdrop[output_idx] = screencast_backdrop.clone().unwrap();
+
+ let mut contents = Vec::new();
+ self.render(
+ RenderCtx {
+ target: RenderTarget::Output,
+ renderer,
+ xray: Some(xray),
+ },
+ Point::from((0., 0.)),
+ pos_in_backdrop,
+ zoom,
+ false,
+ &mut |elem| contents.push(elem),
+ );
+ contents_with_blocked_out_bg = Some(contents);
+ } else {
+ xray.background[screencast_idx] = output_background.clone().unwrap();
+ xray.backdrop[screencast_idx] = output_backdrop.clone().unwrap();
+ }
+ }
+
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let mut blocked_out_contents = Vec::new();
self.render(
- renderer,
+ RenderCtx {
+ target: RenderTarget::Screencast,
+ renderer,
+ xray: xray.as_deref(),
+ },
Point::from((0., 0.)),
+ pos_in_backdrop,
+ zoom,
false,
- RenderTarget::Screencast,
&mut |elem| blocked_out_contents.push(elem),
);
+ // Put everything back to normal.
+ if let Some(xray) = &mut xray {
+ if xray_has_blocked_out_layers {
+ xray.background[output_idx] = output_background.take().unwrap();
+ xray.backdrop[output_idx] = output_backdrop.take().unwrap();
+ } else {
+ xray.background[screencast_idx] = screencast_background.take().unwrap();
+ xray.backdrop[screencast_idx] = screencast_backdrop.take().unwrap();
+ }
+ }
+
RenderSnapshot {
contents,
+ contents_with_blocked_out_bg,
blocked_out_contents,
block_out_from: self.window.rules().block_out_from,
size: self.animated_tile_size(),
texture: Default::default(),
+ texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: Default::default(),
}
}
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 0df4662096..15b0712587 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -32,7 +32,8 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
-use crate::render_helpers::RenderTarget;
+use crate::render_helpers::xray::Xray;
+use crate::render_helpers::RenderCtx;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{
@@ -1626,22 +1627,27 @@ impl Workspace {
pub fn render_scrolling(
&self,
- renderer: &mut R,
- target: RenderTarget,
+ ctx: RenderCtx,
+ pos_in_backdrop: Point,
+ zoom: f64,
focus_ring: bool,
push: &mut dyn FnMut(WorkspaceRenderElement),
) {
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
- self.scrolling
- .render(renderer, target, scrolling_focus_ring, &mut |elem| {
- push(elem.into())
- });
+ self.scrolling.render(
+ ctx,
+ pos_in_backdrop,
+ zoom,
+ scrolling_focus_ring,
+ &mut |elem| push(elem.into()),
+ );
}
pub fn render_floating(
&self,
- renderer: &mut R,
- target: RenderTarget,
+ ctx: RenderCtx,
+ pos_in_backdrop: Point,
+ zoom: f64,
focus_ring: bool,
push: &mut dyn FnMut(WorkspaceRenderElement),
) {
@@ -1652,9 +1658,10 @@ impl Workspace {
let view_rect = Rectangle::from_size(self.view_size);
let floating_focus_ring = focus_ring && self.floating_is_active();
self.floating.render(
- renderer,
+ ctx,
+ pos_in_backdrop,
+ zoom,
view_rect,
- target,
floating_focus_ring,
&mut |elem| push(elem.into()),
);
@@ -1689,14 +1696,29 @@ impl Workspace {
) || !self.render_above_top_layer()
}
- pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
+ pub fn store_unmap_snapshot_if_empty(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ xray: Option<&mut Xray>,
+ xray_has_blocked_out_layers: bool,
+ window: &W::Id,
+ pos_in_backdrop: Point,
+ zoom: f64,
+ ) {
let view_size = self.view_size();
for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) {
if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
let view_rect = Rectangle::new(view_pos, view_size);
tile.update_render_elements(false, view_rect);
- tile.store_unmap_snapshot_if_empty(renderer);
+ let pos_in_backdrop = pos_in_backdrop + tile_pos.upscale(zoom);
+ tile.store_unmap_snapshot_if_empty(
+ renderer,
+ xray,
+ xray_has_blocked_out_layers,
+ pos_in_backdrop,
+ zoom,
+ );
return;
}
}
diff --git a/src/niri.rs b/src/niri.rs
index 414f702d80..241c5dcbd0 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -70,6 +70,7 @@ use smithay::utils::{
ClockSource, IsAlive as _, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size,
Transform, SERIAL_COUNTER,
};
+use smithay::wayland::background_effect::BackgroundEffectState;
use smithay::wayland::compositor::{
with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
CompositorState, HookId, SurfaceData, TraversalAction,
@@ -144,19 +145,22 @@ use crate::niri_render_elements;
use crate::protocols::ext_workspace::{self, ExtWorkspaceManagerState};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
use crate::protocols::gamma_control::GammaControlManagerState;
+use crate::protocols::kde_blur::KdeBlurState;
use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState;
use crate::protocols::output_management::OutputManagementManagerState;
use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManagerState};
use crate::protocols::virtual_pointer::VirtualPointerManagerState;
+use crate::render_helpers::blur::BlurOptions;
use crate::render_helpers::debug::push_opaque_regions;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::surface::push_elements_from_surface_tree;
use crate::render_helpers::texture::TextureBuffer;
+use crate::render_helpers::xray::Xray;
use crate::render_helpers::{
encompassing_geo, render_to_dmabuf, render_to_encompassing_texture, render_to_shm,
- render_to_texture, render_to_vec, shaders, RenderTarget,
+ render_to_texture, render_to_vec, shaders, RenderCtx, RenderTarget,
};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::screencasting::Screencasting;
@@ -277,6 +281,8 @@ pub struct Niri {
pub screencopy_state: ScreencopyManagerState,
pub output_management_state: OutputManagementManagerState,
pub viewporter_state: ViewporterState,
+ pub background_effect_state: BackgroundEffectState,
+ pub kde_blur_state: KdeBlurState,
pub xdg_foreign_state: XdgForeignState,
pub shm_state: ShmState,
pub output_manager_state: OutputManagerState,
@@ -476,6 +482,7 @@ pub struct OutputState {
/// Solid color buffer for the backdrop that we use instead of clearing to avoid damage
/// tracking issues and make screenshots easier.
pub backdrop_buffer: SolidColorBuffer,
+ pub xray: Xray,
pub lock_render_state: LockRenderState,
pub lock_surface: Option,
pub lock_color_buffer: SolidColorBuffer,
@@ -2029,6 +2036,50 @@ impl State {
self.niri.queue_redraw_all();
}
+ pub fn store_unmap_snapshot(&mut self, window: &Window, output: Option<&Output>) {
+ // We'll be rendering Tiles and other stuff that needs their render elements up to date.
+ self.niri.update_render_elements(output);
+
+ self.backend.with_primary_renderer(|renderer| {
+ if let Some(output) = output {
+ let mut ctx = RenderCtx {
+ target: RenderTarget::Output,
+ renderer,
+ xray: None,
+ };
+
+ self.niri.fill_xray_elements(ctx.r(), output);
+
+ // If any background layer has block_out_from, also fill the Screencast xray
+ // buffer so the unmap snapshot can render a buffer with blocked-out background.
+ //
+ // This will be used in Tile::render_snapshot().
+ let has_blocked_out = self.niri.has_blocked_out_background_layers(output);
+ if has_blocked_out {
+ let screencast_ctx = RenderCtx {
+ target: RenderTarget::Screencast,
+ ..ctx.r()
+ };
+ self.niri.fill_xray_elements(screencast_ctx, output);
+ }
+
+ let state = self.niri.output_state.get_mut(output).unwrap();
+ self.niri.layout.store_unmap_snapshot(
+ renderer,
+ Some(&mut state.xray),
+ has_blocked_out,
+ window,
+ );
+
+ self.niri.clear_xray_elements(output);
+ } else {
+ self.niri
+ .layout
+ .store_unmap_snapshot(renderer, None, false, window);
+ }
+ });
+ }
+
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub fn set_dynamic_cast_target(&mut self, _target: CastTarget) {}
@@ -2285,6 +2336,8 @@ impl Niri {
let screencopy_state =
ScreencopyManagerState::new::(&display_handle, client_is_unrestricted);
let viewporter_state = ViewporterState::new::(&display_handle);
+ let background_effect_state = BackgroundEffectState::new::(&display_handle);
+ let kde_blur_state = KdeBlurState::new::(&display_handle);
let xdg_foreign_state = XdgForeignState::new::(&display_handle);
let is_tty = matches!(backend, Backend::Tty(_));
@@ -2468,6 +2521,8 @@ impl Niri {
output_management_state,
screencopy_state,
viewporter_state,
+ background_effect_state,
+ kde_blur_state,
xdg_foreign_state,
text_input_state,
input_method_state,
@@ -2811,6 +2866,7 @@ impl Niri {
vblank_throttle: VBlankThrottle::new(self.event_loop.clone(), name.connector.clone()),
frame_callback_sequence: 0,
backdrop_buffer: SolidColorBuffer::new(size, backdrop_color),
+ xray: Xray::new(),
lock_render_state,
lock_surface: None,
lock_color_buffer: SolidColorBuffer::new(size, CLEAR_COLOR_LOCKED),
@@ -3997,6 +4053,27 @@ impl Niri {
if output.is_none_or(|output| out == output) {
let scale = Scale::from(out.current_scale().fractional_scale());
let transform = out.current_transform();
+ let mode = out.current_mode().unwrap();
+ let size = transform.transform_size(mode.size);
+
+ state.xray.workspaces.clear();
+ let mon = self.layout.monitor_for_output(out).unwrap();
+ for (ws, geo) in mon.workspaces_with_render_geo() {
+ let bg_color = ws.render_background().color();
+ state.xray.workspaces.push((geo, bg_color));
+ }
+ state.xray.backdrop_color = state.backdrop_buffer.color();
+ let blur_options = BlurOptions::from(self.config.borrow().blur);
+ for buf in &state.xray.background {
+ let mut buffer = buf.borrow_mut();
+ buffer.update_size(size, scale);
+ buffer.update_blur_options(blur_options);
+ }
+ for buf in &state.xray.backdrop {
+ let mut buffer = buf.borrow_mut();
+ buffer.update_size(size, scale);
+ buffer.update_blur_options(blur_options);
+ }
if let Some(transition) = &mut state.screen_transition {
transition.update_render_elements(scale, transform);
@@ -4025,39 +4102,57 @@ impl Niri {
}
}
- pub fn render(
+ pub fn render_to_vec(
&self,
- renderer: &mut R,
+ ctx: RenderCtx,
output: &Output,
include_pointer: bool,
- target: RenderTarget,
) -> Vec> {
let mut elements = Vec::new();
- self.render_inner(renderer, output, include_pointer, target, &mut |elem| {
+ self.render(ctx, output, include_pointer, &mut |elem| {
elements.push(elem)
});
elements
}
- pub fn render_inner(
+ pub fn render(
&self,
- renderer: &mut R,
+ mut ctx: RenderCtx,
output: &Output,
include_pointer: bool,
- mut target: RenderTarget,
push: &mut dyn FnMut(OutputRenderElements),
) {
let _span = tracy_client::span!("Niri::render");
- if target == RenderTarget::Output {
+ if ctx.target == RenderTarget::Output {
if let Some(preview) = self.config.borrow().debug.preview_render {
- target = match preview {
+ ctx.target = match preview {
PreviewRender::Screencast => RenderTarget::Screencast,
PreviewRender::ScreenCapture => RenderTarget::ScreenCapture,
};
}
}
+ self.fill_xray_elements(ctx.as_gles(), output);
+
+ // Reborrow to shorten lifetime to be able to put in xray.
+ let mut ctx = ctx.r();
+ let state = self.output_state.get(output).unwrap();
+ ctx.xray = Some(&state.xray);
+
+ self.render_inner(ctx, output, include_pointer, push);
+
+ self.clear_xray_elements(output);
+ }
+
+ fn render_inner(
+ &self,
+ mut ctx: RenderCtx,
+ output: &Output,
+ include_pointer: bool,
+ push: &mut dyn FnMut(OutputRenderElements),
+ ) {
+ let state = self.output_state.get(output).unwrap();
let output_scale = Scale::from(output.current_scale().fractional_scale());
let push = if self.debug_draw_opaque_regions {
@@ -4071,32 +4166,30 @@ impl Niri {
// The pointer goes on the top.
if include_pointer && self.pointer_visibility.is_visible() {
- self.render_pointer(renderer, output, &mut |elem| push(elem.into()));
+ self.render_pointer(ctx.renderer, output, &mut |elem| push(elem.into()));
}
// Next, the screen transition texture.
{
- let state = self.output_state.get(output).unwrap();
if let Some(transition) = &state.screen_transition {
- push(transition.render(target).into());
+ push(transition.render(ctx.target).into());
}
}
// Next, the exit confirm dialog.
self.exit_confirm_dialog
- .render(renderer, output, &mut |elem| push(elem.into()));
+ .render(ctx.renderer, output, &mut |elem| push(elem.into()));
// Next, the config error notification too.
- if let Some(element) = self.config_error_notification.render(renderer, output) {
+ if let Some(element) = self.config_error_notification.render(ctx.renderer, output) {
push(element.into());
}
// If the session is locked, draw the lock surface.
if self.is_locked() {
- let state = self.output_state.get(output).unwrap();
if let Some(surface) = state.lock_surface.as_ref() {
push_elements_from_surface_tree(
- renderer,
+ ctx.renderer,
surface.wl_surface(),
Point::new(0, 0),
output_scale,
@@ -4121,7 +4214,6 @@ impl Niri {
}
// Prepare the background elements.
- let state = self.output_state.get(output).unwrap();
let backdrop = SolidColorRenderElement::from_buffer(
&state.backdrop_buffer,
(0., 0.),
@@ -4133,7 +4225,7 @@ impl Niri {
// If the screenshot UI is open, draw it.
if self.screenshot_ui.is_open() {
self.screenshot_ui
- .render_output(output, target, &mut |elem| push(elem.into()));
+ .render_output(output, ctx.target, &mut |elem| push(elem.into()));
// Add the backdrop for outputs that were connected while the screenshot UI was open.
push(backdrop);
@@ -4142,15 +4234,13 @@ impl Niri {
}
// Draw the hotkey overlay on top.
- if let Some(element) = self.hotkey_overlay.render(renderer, output) {
+ if let Some(element) = self.hotkey_overlay.render(ctx.renderer, output) {
push(element.into());
}
// Then, the Alt-Tab switcher.
self.window_mru_ui
- .render_output(self, output, renderer, target, &mut |elem| {
- push(elem.into())
- });
+ .render_output(self, output, ctx.r(), &mut |elem| push(elem.into()));
// Don't draw the focus ring on the workspaces while interactively moving above those
// workspaces, since the interactively-moved window already has a focus ring.
@@ -4167,7 +4257,7 @@ impl Niri {
// into different functions).
macro_rules! push_popups_from_layer {
($layer:expr, $backdrop:expr, $push:expr) => {{
- self.render_layer_popups(renderer, target, &layer_map, $layer, $backdrop, $push);
+ self.render_layer_popups(ctx.r(), &layer_map, $layer, $backdrop, $push);
}};
($layer:expr, true) => {{
push_popups_from_layer!($layer, true, &mut |elem| push(elem.into()));
@@ -4181,7 +4271,15 @@ impl Niri {
}
macro_rules! push_normal_from_layer {
($layer:expr, $backdrop:expr, $push:expr) => {{
- self.render_layer_normal(renderer, target, &layer_map, $layer, $backdrop, $push);
+ self.render_layer_normal(
+ ctx.r(),
+ &layer_map,
+ $layer,
+ Point::new(0., 0.),
+ 1.,
+ $backdrop,
+ $push,
+ );
}};
($layer:expr, true) => {{
push_normal_from_layer!($layer, true, &mut |elem| push(elem.into()));
@@ -4202,13 +4300,11 @@ impl Niri {
// Otherwise, we will render all layer-shell pop-ups and the top layer on top.
if mon.render_above_top_layer() {
self.layout
- .render_interactive_move_for_output(renderer, output, target, &mut |elem| {
- push(elem.into())
- });
+ .render_interactive_move_for_output(ctx.r(), output, &mut |elem| push(elem.into()));
- mon.render_insert_hint_between_workspaces(renderer, &mut |elem| push(elem.into()));
+ mon.render_insert_hint_between_workspaces(ctx.renderer, &mut |elem| push(elem.into()));
- mon.render_workspaces(renderer, target, focus_ring, &mut |elem| push(elem.into()));
+ mon.render_workspaces(ctx.r(), focus_ring, &mut |elem| push(elem.into()));
push_popups_from_layer!(Layer::Top);
push_normal_from_layer!(Layer::Top);
@@ -4227,11 +4323,9 @@ impl Niri {
push_normal_from_layer!(Layer::Top);
self.layout
- .render_interactive_move_for_output(renderer, output, target, &mut |elem| {
- push(elem.into())
- });
+ .render_interactive_move_for_output(ctx.r(), output, &mut |elem| push(elem.into()));
- mon.render_insert_hint_between_workspaces(renderer, &mut |elem| push(elem.into()));
+ mon.render_insert_hint_between_workspaces(ctx.renderer, &mut |elem| push(elem.into()));
// Macro instead of closure to avoid borrowing push().
macro_rules! process {
@@ -4249,17 +4343,28 @@ impl Niri {
push_popups_from_layer!(Layer::Background, process!(geo));
}
- mon.render_workspaces(renderer, target, focus_ring, &mut |elem| push(elem.into()));
+ mon.render_workspaces(ctx.r(), focus_ring, &mut |elem| push(elem.into()));
for (ws, geo) in mon.workspaces_with_render_geo() {
- push_normal_from_layer!(Layer::Bottom, process!(geo));
- push_normal_from_layer!(Layer::Background, process!(geo));
+ let mut push_normal = |layer| {
+ self.render_layer_normal(
+ ctx.r(),
+ &layer_map,
+ layer,
+ geo.loc,
+ zoom,
+ false,
+ process!(geo),
+ )
+ };
+ push_normal(Layer::Bottom);
+ push_normal(Layer::Background);
process!(geo)(ws.render_background());
}
}
- mon.render_workspace_shadows(renderer, &mut |elem| push(elem.into()));
+ mon.render_workspace_shadows(ctx.renderer, &mut |elem| push(elem.into()));
// Then the backdrop.
push_popups_from_layer!(Layer::Background, true);
@@ -4268,6 +4373,92 @@ impl Niri {
push(backdrop);
}
+ pub fn fill_xray_elements(&self, mut ctx: RenderCtx, output: &Output) {
+ let _span = tracy_client::span!("Niri::fill_xray_elements");
+
+ // Make sure the xrayed elements themselves cannot use xray by mistake.
+ ctx.xray = None;
+
+ let state = self.output_state.get(output).unwrap();
+ let xray = &state.xray;
+ let layer_map = layer_map_for_output(output);
+
+ // FIXME: it would be cool to call this code on-demand. It's even relatively simple to do:
+ // move this function to after the render_inner() call, check if
+ // Rc::strong_count(&xray.background) > 1, and only then construct the elements. This way,
+ // only if something referenced the xray buffer will the elements get constructed.
+ //
+ // Unfortunately, currently this runs into an important limitation: offscreens are rendered
+ // immediately deep inside render_inner(), and when they are, they already need the xray
+ // elements filled.
+ //
+ // Perhaps in the future when offscreen rendering becomes on-demand, this optimization will
+ // be possible.
+
+ let mut buffer = xray.background[ctx.target as usize].borrow_mut();
+ {
+ let elements = buffer.elements();
+ elements.clear();
+ self.render_layer_normal(
+ ctx.r(),
+ &layer_map,
+ Layer::Background,
+ Point::new(0., 0.),
+ 1.,
+ false,
+ &mut |elem| elements.push(elem.into()),
+ );
+ // Avoid unused capacity remaining forever.
+ elements.shrink_to_fit();
+ }
+
+ let mut buffer = xray.backdrop[ctx.target as usize].borrow_mut();
+ {
+ let elements = buffer.elements();
+ elements.clear();
+ self.render_layer_normal(
+ ctx.r(),
+ &layer_map,
+ Layer::Background,
+ Point::new(0., 0.),
+ 1.,
+ true,
+ &mut |elem| elements.push(elem.into()),
+ );
+ // Avoid unused capacity remaining forever.
+ elements.shrink_to_fit();
+ }
+ }
+
+ pub fn clear_xray_elements(&self, output: &Output) {
+ let state = self.output_state.get(output).unwrap();
+ let xray = &state.xray;
+
+ // Clear the xray elements for all render targets after all rendering that could use them
+ // did so.
+ for buf in &xray.background {
+ buf.borrow_mut().elements().clear();
+ }
+ for buf in &xray.backdrop {
+ buf.borrow_mut().elements().clear();
+ }
+ }
+
+ /// Checks if any background layer surface has `block_out_from` set.
+ pub fn has_blocked_out_background_layers(&self, output: &Output) -> bool {
+ let layer_map = layer_map_for_output(output);
+ for for_backdrop in [false, true] {
+ for (mapped, _geo) in
+ self.layers_in_render_order(&layer_map, Layer::Background, for_backdrop)
+ {
+ if mapped.rules().block_out_from.is_some() {
+ return true;
+ }
+ }
+ }
+ false
+ }
+
fn layers_in_render_order<'a>(
&'a self,
layer_map: &'a LayerMap,
@@ -4287,31 +4478,34 @@ impl Niri {
})
}
+ #[allow(clippy::too_many_arguments)]
fn render_layer_normal(
&self,
- renderer: &mut R,
- target: RenderTarget,
+ mut ctx: RenderCtx,
layer_map: &LayerMap,
layer: Layer,
+ pos_in_backdrop: Point,
+ zoom: f64,
for_backdrop: bool,
push: &mut dyn FnMut(LayerSurfaceRenderElement),
) {
for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) {
- mapped.render_normal(renderer, geo.loc.to_f64(), target, push);
+ let loc = geo.loc.to_f64();
+ let pos_in_backdrop = pos_in_backdrop + loc.upscale(zoom);
+ mapped.render_normal(ctx.r(), loc, pos_in_backdrop, zoom, push);
}
}
fn render_layer_popups(
&self,
- renderer: &mut R,
- target: RenderTarget,
+ mut ctx: RenderCtx,
layer_map: &LayerMap,
layer: Layer,
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(renderer, geo.loc.to_f64(), target, push);
+ mapped.render_popups(ctx.r(), geo.loc.to_f64(), push);
}
}
@@ -4935,7 +5129,12 @@ impl Niri {
if let Some(screencopy) = screencopy {
if screencopy.output() == output {
let elements = elements.get_or_init(|| {
- self.render(renderer, output, true, RenderTarget::ScreenCapture)
+ let ctx = RenderCtx {
+ renderer,
+ target: RenderTarget::ScreenCapture,
+ xray: None,
+ };
+ self.render_to_vec(ctx, output, true)
});
// FIXME: skip elements if not including pointers
let render_result = Self::render_for_screencopy_internal(
@@ -4998,12 +5197,13 @@ impl Niri {
self.update_render_elements(Some(output));
- let elements = self.render(
+ let ctx = RenderCtx {
renderer,
- output,
- screencopy.overlay_cursor(),
- RenderTarget::ScreenCapture,
- );
+ target: RenderTarget::ScreenCapture,
+ xray: None,
+ };
+ let elements = self.render_to_vec(ctx, output, screencopy.overlay_cursor());
+
let Some(damage_tracker) = self.screencopy_state.damage_tracker(manager) else {
error!("screencopy queue must not be deleted as long as frames exist");
bail!("screencopy queue missing");
@@ -5126,7 +5326,12 @@ impl Niri {
RenderTarget::ScreenCapture,
];
let screenshot = targets.map(|target| {
- let elements = self.render::(renderer, &output, false, target);
+ let ctx = RenderCtx {
+ renderer,
+ target,
+ xray: None,
+ };
+ let elements = self.render_to_vec(ctx, &output, false);
let elements = elements.iter().rev();
let res = render_to_texture(
@@ -5203,12 +5408,12 @@ impl Niri {
let size = transform.transform_size(size);
let scale = Scale::from(output.current_scale().fractional_scale());
- let elements = self.render::(
+ let ctx = RenderCtx {
renderer,
- output,
- include_pointer,
- RenderTarget::ScreenCapture,
- );
+ target: RenderTarget::ScreenCapture,
+ xray: None,
+ };
+ let elements = self.render_to_vec(ctx, output, include_pointer);
let elements = elements.iter().rev();
let pixels = render_to_vec(
renderer,
@@ -5258,12 +5463,16 @@ impl Niri {
}
let pointer_count = elements.len();
- mapped.render(
+ let ctx = RenderCtx {
renderer,
+ target: RenderTarget::ScreenCapture,
+ xray: None,
+ };
+ mapped.render(
+ ctx,
mapped.window.geometry().loc.to_f64(),
scale,
alpha,
- RenderTarget::ScreenCapture,
&mut |elem| elements.push(elem.into()),
);
@@ -5419,12 +5628,12 @@ impl Niri {
let transform = output.current_transform();
let size = transform.transform_size(size);
- let elements = self.render::(
+ let ctx = RenderCtx {
renderer,
- &output,
- include_pointer,
- RenderTarget::ScreenCapture,
- );
+ target: RenderTarget::ScreenCapture,
+ xray: None,
+ };
+ let elements = self.render_to_vec(ctx, &output, include_pointer);
let elements = elements.iter().rev();
let pixels = render_to_vec(
renderer,
@@ -5897,7 +6106,12 @@ impl Niri {
RenderTarget::ScreenCapture,
];
let textures = targets.map(|target| {
- let elements = self.render::(renderer, &output, false, target);
+ let ctx = RenderCtx {
+ renderer,
+ target,
+ xray: None,
+ };
+ let elements = self.render_to_vec(ctx, &output, false);
let elements = elements.iter().rev();
let res = render_to_texture(
diff --git a/src/protocols/kde_blur.rs b/src/protocols/kde_blur.rs
new file mode 100644
index 0000000000..e019f90400
--- /dev/null
+++ b/src/protocols/kde_blur.rs
@@ -0,0 +1,214 @@
+use std::sync::Mutex;
+
+use smithay::reexports::wayland_server;
+use smithay::wayland::compositor::{
+ get_region_attributes, with_states, Cacheable, RegionAttributes,
+};
+use wayland_protocols_plasma::blur::server::org_kde_kwin_blur::{self, OrgKdeKwinBlur};
+use wayland_protocols_plasma::blur::server::org_kde_kwin_blur_manager::{
+ self, OrgKdeKwinBlurManager,
+};
+use wayland_server::backend::GlobalId;
+use wayland_server::protocol::wl_surface::WlSurface;
+use wayland_server::{
+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak,
+};
+
+pub trait KdeBlurHandler:
+ GlobalDispatch
+ + Dispatch
+ + Dispatch
+ + 'static
+{
+ /// Called when a blur region becomes pending on a surface, awaiting a commit.
+ fn set_blur_region(&mut self, wl_surface: WlSurface) {
+ let _ = wl_surface;
+ }
+
+ /// Called when a blur region unset becomes pending on a surface, awaiting a commit.
+ fn unset_blur_region(&mut self, wl_surface: WlSurface) {
+ let _ = wl_surface;
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct KdeBlurSurfaceCachedState {
+ /// Region of the surface that will have its background blurred.
+ ///
+ /// `None` means no blurring.
+ pub blur_region: Option,
+}
+
+#[derive(Debug, Clone, Default)]
+pub enum KdeBlurRegion {
+ #[default]
+ WholeSurface,
+ Region(RegionAttributes),
+}
+
+impl Cacheable for KdeBlurSurfaceCachedState {
+ fn commit(&mut self, _dh: &DisplayHandle) -> Self {
+ self.clone()
+ }
+
+ fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) {
+ *into = self;
+ }
+}
+
+#[derive(Debug)]
+pub struct KdeBlurSurfaceUserData {
+ surface: Weak,
+ pending_region: Mutex,
+}
+
+impl KdeBlurSurfaceUserData {
+ fn new(surface: WlSurface) -> Self {
+ Self {
+ surface: surface.downgrade(),
+ pending_region: Mutex::new(KdeBlurRegion::WholeSurface),
+ }
+ }
+
+ fn wl_surface(&self) -> Option {
+ self.surface.upgrade().ok()
+ }
+}
+
+#[derive(Debug)]
+pub struct KdeBlurState {
+ global: GlobalId,
+}
+
+impl KdeBlurState {
+ pub fn new(display: &DisplayHandle) -> KdeBlurState {
+ let global = display.create_global::(1, ());
+ KdeBlurState { global }
+ }
+
+ pub fn global(&self) -> GlobalId {
+ self.global.clone()
+ }
+}
+
+impl GlobalDispatch for KdeBlurState {
+ fn bind(
+ _state: &mut D,
+ _handle: &DisplayHandle,
+ _client: &Client,
+ resource: New,
+ _global_data: &(),
+ data_init: &mut DataInit<'_, D>,
+ ) {
+ let _manager = data_init.init(resource, ());
+ }
+}
+
+impl Dispatch for KdeBlurState {
+ fn request(
+ state: &mut D,
+ _client: &Client,
+ _manager: &OrgKdeKwinBlurManager,
+ request: org_kde_kwin_blur_manager::Request,
+ _data: &(),
+ _dh: &DisplayHandle,
+ data_init: &mut DataInit<'_, D>,
+ ) {
+ match request {
+ org_kde_kwin_blur_manager::Request::Create { id, surface } => {
+ data_init.init(id, KdeBlurSurfaceUserData::new(surface));
+ }
+ org_kde_kwin_blur_manager::Request::Unset { surface } => {
+ with_states(&surface, |states| {
+ let mut cached = states.cached_state.get::();
+ let pending = cached.pending();
+ pending.blur_region = None;
+ });
+ state.unset_blur_region(surface);
+ }
+ _ => {}
+ }
+ }
+}
+
+impl Dispatch for KdeBlurState {
+ fn request(
+ state: &mut D,
+ _client: &Client,
+ _obj: &OrgKdeKwinBlur,
+ request: org_kde_kwin_blur::Request,
+ data: &KdeBlurSurfaceUserData,
+ _dh: &DisplayHandle,
+ _data_init: &mut DataInit<'_, D>,
+ ) {
+ match request {
+ org_kde_kwin_blur::Request::SetRegion { region } => {
+ let region = region.as_ref().map(get_region_attributes);
+
+ // In the KDE blur protocol, an empty region means whole surface.
+ let region = match region {
+ Some(region) if !region.rects.is_empty() => KdeBlurRegion::Region(region),
+ _ => KdeBlurRegion::WholeSurface,
+ };
+
+ *data.pending_region.lock().unwrap() = region;
+ }
+ org_kde_kwin_blur::Request::Commit => {
+ let Some(surface) = data.wl_surface() else {
+ return;
+ };
+
+ with_states(&surface, |states| {
+ let mut cached = states.cached_state.get::();
+ let pending = cached.pending();
+ let region = data.pending_region.lock().unwrap().clone();
+ pending.blur_region = Some(region);
+ });
+ state.set_blur_region(surface);
+ }
+ org_kde_kwin_blur::Request::Release => {
+ // No-op.
+ }
+ _ => {}
+ }
+ }
+
+ fn destroyed(
+ _state: &mut D,
+ _client_id: wayland_server::backend::ClientId,
+ _object: &OrgKdeKwinBlur,
+ _data: &KdeBlurSurfaceUserData,
+ ) {
+ // No-op: cleanup is handled by double-buffering and surface destruction
+ }
+}
+
+#[macro_export]
+macro_rules! delegate_kde_blur {
+ ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
+ const _: () = {
+ use smithay::reexports::wayland_server;
+ use wayland_protocols_plasma::blur::server::{
+ org_kde_kwin_blur_manager::OrgKdeKwinBlurManager,
+ org_kde_kwin_blur::OrgKdeKwinBlur,
+ };
+ use wayland_server::{delegate_dispatch, delegate_global_dispatch};
+ use $crate::protocols::kde_blur::{KdeBlurState, KdeBlurSurfaceUserData};
+
+ delegate_global_dispatch!(
+ $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
+ $ty: [OrgKdeKwinBlurManager: ()] => KdeBlurState
+ );
+
+ delegate_dispatch!(
+ $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
+ $ty: [OrgKdeKwinBlurManager: ()] => KdeBlurState
+ );
+
+ delegate_dispatch!(
+ $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
+ $ty: [OrgKdeKwinBlur: KdeBlurSurfaceUserData] => KdeBlurState
+ );
+ };
+ };
+}
diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs
index 1a81750d88..167a932f7e 100644
--- a/src/protocols/mod.rs
+++ b/src/protocols/mod.rs
@@ -1,6 +1,7 @@
pub mod ext_workspace;
pub mod foreign_toplevel;
pub mod gamma_control;
+pub mod kde_blur;
pub mod mutter_x11_interop;
pub mod output_management;
pub mod screencopy;
diff --git a/src/render_helpers/background_effect.rs b/src/render_helpers/background_effect.rs
new file mode 100644
index 0000000000..85cb8882c7
--- /dev/null
+++ b/src/render_helpers/background_effect.rs
@@ -0,0 +1,275 @@
+use std::array;
+use std::sync::Arc;
+
+use niri_config::CornerRadius;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale};
+
+use crate::niri_render_elements;
+use crate::render_helpers::blur::BlurOptions;
+use crate::render_helpers::damage::ExtraDamage;
+use crate::render_helpers::framebuffer_effect::{FramebufferEffect, FramebufferEffectElement};
+use crate::render_helpers::xray::XrayElement;
+use crate::render_helpers::{RenderCtx, RenderTarget};
+
+#[derive(Debug)]
+pub struct BackgroundEffect {
+ // Framebuffer effects are per-render-target because they store the framebuffer contents in a
+ // texture, and those differ per render target.
+ nonxray: [FramebufferEffect; RenderTarget::COUNT],
+ /// Damage when options change.
+ damage: ExtraDamage,
+ /// Corner radius for clipping.
+ ///
+ /// Stored here in addition to `RenderParams` to damage when it changes.
+ // FIXME: would be good to remove this duplication of radius.
+ corner_radius: CornerRadius,
+ blur_config: niri_config::Blur,
+ options: Options,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct Options {
+ pub blur: bool,
+ pub xray: bool,
+ pub noise: Option,
+ pub saturation: Option,
+}
+
+impl Options {
+ fn is_visible(&self) -> bool {
+ self.xray
+ || self.blur
+ || self.noise.is_some_and(|x| x > 0.)
+ || self.saturation.is_some_and(|x| x != 1.)
+ }
+}
+
+/// Render-time parameters.
+#[derive(Debug)]
+pub struct RenderParams {
+ /// Geometry of the background effect.
+ pub geometry: Rectangle