mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ed0da44d9 | |||
| 91be662ac6 | |||
| 4438aefc8d | |||
| 6c4dfd7772 | |||
| 1ad422f0db | |||
| 8fd9fb73f2 | |||
| 8d83fbae67 | |||
| 414729dce5 | |||
| dbe79b7873 | |||
| 9438f59e2b | |||
| 719255ac35 | |||
| 8a51935224 | |||
| 8d583fe854 | |||
| 74d2b18603 | |||
| fad02316f1 | |||
| 47385c2ecd | |||
| e430d3ab2b | |||
| e472b5b0f1 | |||
| efb169416d | |||
| 3a3a97ec2a | |||
| e9c182a13c | |||
| cfe059c303 | |||
| bd7c748a4f | |||
| 68bb942d21 | |||
| 04c422e43f | |||
| d09fa2709c | |||
| 2c3315aebb | |||
| 5a45088061 | |||
| 404d6dccc4 | |||
| 084f2cb193 | |||
| 25c88b542f | |||
| 6fc50a1fb8 | |||
| 5e11b96f12 | |||
| 849d26d646 | |||
| 9e5716a9db | |||
| f4ebbc8017 | |||
| ce9dd33213 | |||
| 10995ec62c | |||
| c814c656c5 | |||
| 82d4c7569e | |||
| 4f0db78248 | |||
| 3e250cdc12 | |||
| a1b0bd6d1c | |||
| 892470afd3 | |||
| f1cb02cfab | |||
| 5dc4e83ba7 | |||
| 2b58e03d30 | |||
| d4b4407236 | |||
| 26ff5f4bf1 | |||
| d7905e6b74 | |||
| 71d7fa9a61 | |||
| 707f08559c | |||
| 4d21489101 | |||
| 5a24aae560 | |||
| 9170161a0a | |||
| 66d66d6030 | |||
| 19866f8b0b | |||
| 4bc3ede4b7 | |||
| 250aa1f3cb | |||
| 0117d6953d | |||
| 931123f38c | |||
| 73c0ce75d8 | |||
| 1b1715fe9b | |||
| fee8719299 | |||
| 7f9c7d1415 | |||
| b81cb13c2c | |||
| 45582ad095 | |||
| b3f5255bb9 | |||
| 8c169b1a14 | |||
| 525b33777b | |||
| dec0e3bf5a | |||
| 6bcaaf9d21 | |||
| f022b3c504 | |||
| ab10a260fa | |||
| 0eddd16b8a | |||
| d020d986ed | |||
| 5abeb923de | |||
| dd1f28998f | |||
| 874e7fd70e | |||
| 599db847f8 | |||
| d1a0380eed | |||
| 8f48f56fe1 | |||
| b07bde3ee8 | |||
| bf142e0b48 | |||
| 8f75d171b6 | |||
| cbf4631461 | |||
| a217ad6424 | |||
| f4dc10e0b4 | |||
| b82d52705e | |||
| c7fa5f29d6 | |||
| e708f54615 | |||
| 2dc6f4482c | |||
| a2a5291175 | |||
| 1fa0338a17 | |||
| 8e3e93b624 | |||
| c1146c0bef | |||
| 41b5de8769 | |||
| 8d9bc2a5c9 | |||
| 6d5c5f12b2 | |||
| 42b2aeb6e6 | |||
| ab47f5cec4 | |||
| 549148d277 | |||
| 189917c933 |
@@ -1,9 +1,9 @@
|
||||
contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
|
||||
url: https://github.com/niri-wm/niri/discussions/new?category=ideas
|
||||
about: Ideas for new features and functionality (start a Discussion)
|
||||
- name: Ask a question
|
||||
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
|
||||
url: https://github.com/niri-wm/niri/discussions/new?category=q-a
|
||||
about: Question about niri (start a Discussion)
|
||||
- name: Matrix room
|
||||
url: https://matrix.to/#/#niri:matrix.org
|
||||
|
||||
+14
-14
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
container: alpine:3
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
PROPTEST_MAX_SHRINK_ITERS: 200000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -230,10 +230,10 @@ jobs:
|
||||
|
||||
fedora:
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:41
|
||||
container: fedora:42
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
CARGO_HOME: /home/runner/work/niri/niri/cargo-home
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
dotnet: false
|
||||
large-packages: false
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -308,7 +308,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
|
||||
@@ -22,15 +22,18 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check for unreplaced "Since:" in the wiki
|
||||
run: |
|
||||
if grep --recursive 'Since: next release' wiki; then
|
||||
exit 1
|
||||
fi
|
||||
# Fail if a match is found (exit code 0)
|
||||
grep --recursive 'Since: next release' docs/wiki && exit 1
|
||||
|
||||
# Fail if grep failed (exit code 2)
|
||||
status=$?
|
||||
if [ $status -ne 1 ]; then exit $status; fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
/target
|
||||
/result
|
||||
|
||||
.idea
|
||||
+2
-2
@@ -31,7 +31,7 @@ I would really appreciate help with testing and reviewing them.
|
||||
### Testing
|
||||
|
||||
Pick a pull request you like, then build it and give it a go.
|
||||
The [Developing niri wiki page](https://yalter.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
|
||||
The [Developing niri wiki page](https://niri-wm.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
|
||||
|
||||
Be really thorough with your testing.
|
||||
We're striving for polished features in niri, so point out any issues and bugs, even small ones like animation jank.
|
||||
@@ -84,7 +84,7 @@ When creating pull requests, please keep the following in mind.
|
||||
- When working on bigger features, I usually start with a big messy commit, then gradually split out smaller self-contained changes from it as the code gets into shape.
|
||||
- [git-rebase.io](https://git-rebase.io/) is a helpful guide for splitting commits and cleaning up history in git.
|
||||
- When you address a review comment, mark it as resolved.
|
||||
- Remember to [run tests](https://yalter.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
|
||||
- Remember to [run tests](https://niri-wm.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
|
||||
- For new layout actions, remember to add them to the randomized tests. For weird Wayland handling, adding client-server tests in `src/tests/` could be very useful.
|
||||
- Test your changes by hand thoroughly, including for edge cases and weird interactions. See the Testing section above for some tips.
|
||||
- Remember to document new config options on the wiki.
|
||||
|
||||
Generated
+708
-523
File diff suppressed because it is too large
Load Diff
+40
-37
@@ -6,37 +6,35 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "25.11.0"
|
||||
version = "26.4.0"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
repository = "https://github.com/niri-wm/niri"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bitflags = "2.10.0"
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
insta = "1.46.0"
|
||||
anyhow = "1.0.102"
|
||||
bitflags = "2.11.1"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
insta = "1.47.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
tracing = { version = "0.1.44", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
# 0.3.20 filters out all ANSI codes to "fix a security issue" while also breaking
|
||||
# everyone who relied on them for color output, with no fallback available.
|
||||
# https://github.com/tokio-rs/tracing/issues/3378
|
||||
tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.4", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay"
|
||||
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.smithay-drm-extras]
|
||||
# version = "0.1.0"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
[package]
|
||||
@@ -53,49 +51,53 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { version = "0.22.0", optional = true }
|
||||
accesskit_unix = { version = "0.18.0", optional = true }
|
||||
# accesskit_unix 0.18 has a regression where it doesn't work in normal configurations.
|
||||
# accesskit 0.21 is its correct dependent version.
|
||||
# https://github.com/niri-wm/niri/issues/3594
|
||||
accesskit = { version = "0.21", optional = true }
|
||||
accesskit_unix = { version = "0.17", optional = true }
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.5.0"
|
||||
async-io = { version = "2.6.0", optional = true }
|
||||
atomic = "0.6.1"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.24.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.3", features = ["executor", "futures-io", "signals"] }
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.4", features = ["executor", "futures-io", "signals"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
clap_complete = "4.5.65"
|
||||
clap_complete_nushell = "4.5.10"
|
||||
clap_complete = "4.6.2"
|
||||
clap_complete_nushell = "4.6.0"
|
||||
directories = "6.0.0"
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.3.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
drm-ffi = "0.9.1"
|
||||
fastrand = "2.4.1"
|
||||
futures-util = { version = "0.3.32", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.30.10"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
glam = "0.32.1"
|
||||
input = { version = "0.10.0", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.180"
|
||||
libc = "0.2.185"
|
||||
libdisplay-info = "0.3.0"
|
||||
log = { version = "0.4.29", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.11.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.11.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.1.0"
|
||||
niri-config = { version = "26.4.0", path = "niri-config" }
|
||||
niri-ipc = { version = "26.4.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.3.0"
|
||||
pango = { version = "0.21.5", features = ["v1_44"] }
|
||||
pangocairo = "0.21.5"
|
||||
pipewire = { version = "0.9.2", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.18.0"
|
||||
png = "0.18.1"
|
||||
profiling = "1.0.17"
|
||||
sd-notify = "0.4.5"
|
||||
sd-notify = "0.5.0"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
wayland-backend = "0.3.12"
|
||||
wayland-scanner = "0.31.8"
|
||||
wayland-backend = "0.3.15"
|
||||
wayland-scanner = "0.31.10"
|
||||
wayland-server = "0.31.13"
|
||||
xcursor = "0.3.10"
|
||||
zbus = { version = "5.13.0", optional = true }
|
||||
zbus = { version = "5.13.2", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -119,14 +121,14 @@ features = [
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.1"
|
||||
insta.workspace = true
|
||||
proptest = "1.9.0"
|
||||
proptest-derive = { version = "0.7.0", features = ["boxed_union"] }
|
||||
rayon = "1.11.0"
|
||||
wayland-client = "0.31.12"
|
||||
proptest = "1.11.0"
|
||||
proptest-derive = { version = "0.8.0", features = ["boxed_union"] }
|
||||
rayon = "1.12.0"
|
||||
wayland-client = "0.31.14"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[build-dependencies]
|
||||
pkg-config = "0.3.32"
|
||||
pkg-config = "0.3.33"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
@@ -147,6 +149,7 @@ dinit = []
|
||||
|
||||
[lints.clippy]
|
||||
new_without_default = "allow"
|
||||
collapsible_match = "allow"
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
@@ -162,7 +165,7 @@ insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "25.11"
|
||||
version = "26.04"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<p align="center">A scrollable-tiling Wayland compositor.</p>
|
||||
<p align="center">
|
||||
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
|
||||
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
||||
<a href="https://github.com/niri-wm/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/niri-wm/niri"></a>
|
||||
<a href="https://github.com/niri-wm/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/niri-wm/niri?logo=github"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://yalter.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://yalter.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup Showcase</a>
|
||||
<a href="https://niri-wm.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://niri-wm.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/niri-wm/niri/discussions/325">Setup Showcase</a>
|
||||
</p>
|
||||
|
||||

|
||||
<img width="1280" height="720" alt="niri with a few windows open" src="https://github.com/user-attachments/assets/dea5909e-1859-4aaa-9d88-d37f9663e00b" />
|
||||
|
||||
## About
|
||||
|
||||
@@ -29,23 +29,24 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
## Features
|
||||
|
||||
- Built from the ground up for scrollable tiling
|
||||
- [Dynamic workspaces](https://yalter.github.io/niri/Workspaces.html) like in GNOME
|
||||
- [Dynamic workspaces](https://niri-wm.github.io/niri/Workspaces.html) like in GNOME
|
||||
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
|
||||
- Built-in screenshot UI
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
- You can [block out](https://yalter.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
|
||||
- [Dynamic cast target](https://yalter.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
|
||||
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||
- Group windows into [tabs](https://yalter.github.io/niri/Tabs.html)
|
||||
- You can [block out](https://niri-wm.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
|
||||
- [Dynamic cast target](https://niri-wm.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
|
||||
- [Touchpad](https://github.com/niri-wm/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/niri-wm/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||
- Group windows into [tabs](https://niri-wm.github.io/niri/Tabs.html)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://yalter.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
|
||||
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
|
||||
- [Gradient borders](https://niri-wm.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
|
||||
- [Background blur](https://niri-wm.github.io/niri/Window-Effects.html) for windows and layer-shell surfaces
|
||||
- [Animations](https://github.com/niri-wm/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/niri-wm/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
|
||||
- Live-reloading config
|
||||
- Works with [screen readers](https://yalter.github.io/niri/Accessibility.html)
|
||||
- Works with [screen readers](https://niri-wm.github.io/niri/Accessibility.html)
|
||||
|
||||
## Video Demo
|
||||
|
||||
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||
https://github.com/niri-wm/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||
|
||||
Also check out this video from Brodie Robertson that showcases a lot of the niri functionality: [Niri Is My New Favorite Wayland Compositor](https://youtu.be/DeYx2exm04M)
|
||||
|
||||
@@ -55,8 +56,8 @@ Niri is stable for day-to-day use and does most things expected of a Wayland com
|
||||
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
|
||||
|
||||
Give it a try!
|
||||
Follow the instructions on the [Getting Started](https://yalter.github.io/niri/Getting-Started.html) page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
Follow the instructions on the [Getting Started](https://niri-wm.github.io/niri/Getting-Started.html) page.
|
||||
Grab a desktop shell like [DankMaterialShell] or [Noctalia] (or build a more traditional setup): niri by itself is not a complete desktop environment.
|
||||
Also check out [awesome-niri], a list of niri-related links and projects.
|
||||
|
||||
Here are some points you may have questions about:
|
||||
@@ -72,7 +73,7 @@ We have touchpad gestures, but no touchscreen gestures yet.
|
||||
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
|
||||
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
|
||||
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
|
||||
- **Xwayland**: [integrated](https://yalter.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
|
||||
- **Xwayland**: [integrated](https://niri-wm.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
|
||||
|
||||
## Media
|
||||
|
||||
@@ -93,7 +94,7 @@ An LWN article with a nice overview and introduction to niri.
|
||||
## Contributing
|
||||
|
||||
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
|
||||
See [CONTRIBUTING.md](https://github.com/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
|
||||
## Inspiration
|
||||
|
||||
@@ -109,8 +110,8 @@ Here are some other projects which implement a similar workflow:
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
- Hyprland has a built-in [scrolling layout](https://wiki.hypr.land/Configuring/Scrolling-Layout/).
|
||||
- [Paneru] and [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Contact
|
||||
|
||||
@@ -121,11 +122,12 @@ We also have a community Discord server: https://discord.gg/vT8Sfjy7sx
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
[awesome-niri]: https://github.com/Vortriz/awesome-niri
|
||||
[awesome-niri]: https://github.com/niri-wm/awesome-niri
|
||||
[karousel]: https://github.com/peterfajdiga/karousel
|
||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[Paneru]: https://github.com/karinushka/paneru
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
|
||||
[OpenTabletDriver]: https://opentabletdriver.net/
|
||||
[DankMaterialShell]: https://danklinux.com/
|
||||
[Noctalia]: https://noctalia.dev/
|
||||
|
||||
@@ -40,5 +40,5 @@ def _badge_for_version(preposition: str, version: str):
|
||||
# we might fail to make real links to release notes on other cases too, but for now this is the one i've found
|
||||
return f"<span class=\"badge\">{preposition}: {version}</span>"
|
||||
else:
|
||||
path = f"https://github.com/YaLTeR/niri/releases/tag/v{version}"
|
||||
path = f"https://github.com/niri-wm/niri/releases/tag/v{version}"
|
||||
return f"<span class=\"badge\">[{preposition}: {version}]({path})</span>"
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
site_name: niri
|
||||
docs_dir: wiki
|
||||
site_url: https://yalter.github.io/niri
|
||||
repo_url: https://github.com/YaLTeR/niri
|
||||
site_url: https://niri-wm.github.io/niri
|
||||
repo_url: https://github.com/niri-wm/niri
|
||||
edit_uri: edit/main/docs/wiki/
|
||||
use_directory_urls: false
|
||||
|
||||
@@ -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
|
||||
@@ -110,6 +111,7 @@ nav:
|
||||
- Design Principles: Development:-Design-Principles.md
|
||||
- Developing niri: Development:-Developing-niri.md
|
||||
- Documenting niri: Development:-Documenting-niri.md
|
||||
- Releasing niri: Development:-Releasing-niri.md
|
||||
- Fractional Layout: Development:-Fractional-Layout.md
|
||||
- Redraw Loop: Development:-Redraw-Loop.md
|
||||
- Animation Timing: Development:-Animation-Timing.md
|
||||
|
||||
@@ -107,6 +107,8 @@ debug {
|
||||
|
||||
### `force-disable-connectors-on-resume`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Force-disables all outputs upon resuming niri (TTY switch or waking up from suspend).
|
||||
This causes a modeset/screen blank on all outputs.
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ Settings from included files will be merged with the settings from the main conf
|
||||
Included config files can in turn include more files.
|
||||
All included files are watched for changes, and the config live-reloads when any of them change.
|
||||
|
||||
You can include by filename or path.
|
||||
|
||||
* Relative to the current file: `other.kdl` or `./other.kdl`
|
||||
* By absolute path: `/path/to/file.kdl`
|
||||
* <sup>Since: 26.04</sup> Home dir paths: `~/file.kdl` expands to `/home/user/file.kdl`
|
||||
|
||||
Includes work only at the top level of the config:
|
||||
|
||||
```kdl,must-fail
|
||||
@@ -116,7 +122,7 @@ window-rule {
|
||||
|
||||
### Optional includes
|
||||
|
||||
<sup>Since: next release</sup>
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
By default, including a nonexistent file will cause an error.
|
||||
You can allow nonexistent includes by setting `optional=true`:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In this section you can configure input devices like keyboard and mouse, and some input-related options.
|
||||
|
||||
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
|
||||
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `trackball`, `tablet`, `touch`.
|
||||
Settings in those sections will apply to every device of that type.
|
||||
Currently, there's no way to configure specific devices individually (but that is planned).
|
||||
|
||||
@@ -89,6 +89,7 @@ input {
|
||||
tablet {
|
||||
// off
|
||||
map-to-output "eDP-1"
|
||||
// map-to-focused-output
|
||||
// left-handed
|
||||
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
|
||||
}
|
||||
@@ -281,6 +282,10 @@ Valid output names are the same as the ones used for output configuration.
|
||||
|
||||
<sup>Since: 0.1.7</sup> When a tablet is not mapped to any output, it will map to the union of all connected outputs, without aspect ratio correction.
|
||||
|
||||
Setting specific to `tablet`:
|
||||
|
||||
- `map-to-focused-output`: <sup>Since: 26.04</sup> will map the tablet to the focused output, takes precedence over `map-to-output`.
|
||||
|
||||
### General Settings
|
||||
|
||||
These settings are not specific to a particular input device.
|
||||
|
||||
@@ -19,7 +19,7 @@ You can find documentation for various sections of the config on these wiki page
|
||||
### Loading
|
||||
|
||||
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
|
||||
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
Please use the default configuration file as the starting point for your custom configuration.
|
||||
|
||||
The configuration is live-reloaded.
|
||||
|
||||
@@ -382,7 +382,7 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
<sup>Since: next release</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
<sup>Since: 26.04</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
|
||||
The pointer will be included only if the window is currently receiving pointer input (usually this means the pointer is on top of the window).
|
||||
|
||||
```kdl
|
||||
|
||||
@@ -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,25 @@ 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
|
||||
}
|
||||
|
||||
popups {
|
||||
opacity 0.5
|
||||
geometry-corner-radius 6
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -69,6 +89,22 @@ layer-rule {
|
||||
}
|
||||
```
|
||||
|
||||
#### `layer`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
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 +227,68 @@ layer-rule {
|
||||
baba-is-float true
|
||||
}
|
||||
```
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
|
||||
|
||||
The properties work the same way as the corresponding layer-rule properties, except that they apply to the layer surface's pop-ups rather than to the layer surface itself.
|
||||
|
||||
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
|
||||
Other properties apply independently.
|
||||
|
||||
> [!NOTE]
|
||||
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
|
||||
>
|
||||
> Some desktop shells will emulate pop-ups by drawing something that looks like a pop-up inside a regular layer surface.
|
||||
> As far as niri is concerned, those are just layer surfaces and not pop-ups, so this block won't apply to them.
|
||||
>
|
||||
> This block also does not affect input-method pop-ups, such as Fcitx.
|
||||
|
||||
```kdl
|
||||
// Blur the background behind Waybar popup menus.
|
||||
layer-rule {
|
||||
match namespace="^waybar$"
|
||||
|
||||
popups {
|
||||
// Match the default GTK 3 popup corner radius.
|
||||
geometry-corner-radius 6
|
||||
opacity 0.85
|
||||
|
||||
background-effect {
|
||||
blur true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the layer surface correctly sets its Wayland geometry to exclude any shadows.
|
||||
Pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
|
||||
|
||||
@@ -177,6 +177,7 @@ layout {
|
||||
### `preset-column-widths`
|
||||
|
||||
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (Mod+Shift+R) to toggle in reverse.
|
||||
|
||||
`proportion` sets the width as a fraction of the output width, taking gaps into account.
|
||||
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
|
||||
@@ -228,7 +229,8 @@ layout {
|
||||
|
||||
<sup>Since: 0.1.9</sup>
|
||||
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
|
||||
Set the heights that the `switch-preset-window-height` action (Mod+Ctrl+Shift+R) toggles between.
|
||||
<sup>Since: 25.08</sup> You can use the `switch-preset-window-height-back` action (not bound by default) to toggle in reverse.
|
||||
|
||||
`proportion` sets the height as a fraction of the output height, taking gaps into account.
|
||||
The default preset heights are <sup>1</sup>⁄<sub>3</sub>, <sup>1</sup>⁄<sub>2</sub> and <sup>2</sup>⁄<sub>3</sub> of the output.
|
||||
|
||||
@@ -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,82 @@ config-notification {
|
||||
disable-failed
|
||||
}
|
||||
```
|
||||
|
||||
### `blur`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Blur configuration that affects all background blur.
|
||||
|
||||
See the [window effects page](./Window-Effects.md) for an overview of background effects.
|
||||
|
||||
```kdl
|
||||
// These are the default values:
|
||||
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` controls 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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -100,6 +100,25 @@ window-rule {
|
||||
tiled-state true
|
||||
baba-is-float true
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
|
||||
popups {
|
||||
opacity 0.5
|
||||
geometry-corner-radius 15
|
||||
|
||||
background-effect {
|
||||
xray true
|
||||
blur true
|
||||
noise 0.05
|
||||
saturation 3
|
||||
}
|
||||
}
|
||||
|
||||
min-width 100
|
||||
max-width 200
|
||||
min-height 300
|
||||
@@ -909,6 +928,95 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509
|
||||
|
||||
</video>
|
||||
|
||||
#### `background-effect`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
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.
|
||||
//
|
||||
// Warning: non-xray blur is currently experimental and has known limitations.
|
||||
// In particular, it doesn't work during window opening and closing animations.
|
||||
window-rule {
|
||||
match is-floating=true
|
||||
|
||||
background-effect {
|
||||
xray false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `popups`
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
Override properties for this window's pop-ups (menus and tooltips).
|
||||
|
||||
The properties work the same way as the corresponding window-rule properties, except that they apply to the window's pop-ups rather than to the window itself.
|
||||
|
||||
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
|
||||
Other properties apply independently.
|
||||
|
||||
> [!NOTE]
|
||||
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
|
||||
>
|
||||
> Examples of things that look like pop-ups that won't work:
|
||||
>
|
||||
> - Fully emulated by the client, i.e. not a pop-up at all, the client just draws something that looks like a pop-up inside its window.
|
||||
> These are common in game engines and in web apps, e.g. the right click menu in Google Docs or in Electron apps like Discord.
|
||||
>
|
||||
> - Uses a wl-subsurface instead of an xdg-popup.
|
||||
> Common in older apps using GTK 3, notably Firefox still uses these for some menus.
|
||||
> Subsurfaces are an indivisible part of a surface and they aren't usually pop-ups, so it wouldn't make sense for niri to apply these rules to them.
|
||||
>
|
||||
> These emulated pop-ups come with other downsides: they cannot reliably extend outside their window, and if the app tries to do that, they will be clipped by rules such as `clip-to-geometry`.
|
||||
> So most modern apps will correctly use xdg-popup, which is the intended way to show pop-ups on Wayland.
|
||||
>
|
||||
> This block also does not affect input-method pop-ups, such as Fcitx.
|
||||
>
|
||||
> For pop-ups created by your desktop shell or desktop components, use the corresponding [layer rule](./Configuration:-Layer-Rules.md#popups).
|
||||
|
||||
```kdl
|
||||
// Blur the background behind pop-up menus in Nautilus.
|
||||
window-rule {
|
||||
match app-id="Nautilus"
|
||||
|
||||
popups {
|
||||
// Matches the default libadwaita pop-up corner radius.
|
||||
geometry-corner-radius 15
|
||||
|
||||
// Note: it'll look better to set background opacity
|
||||
// through your GTK theme CSS and not here.
|
||||
// This is just an example that makes it look obvious.
|
||||
opacity 0.5
|
||||
|
||||
background-effect {
|
||||
blur true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the window correctly sets its Wayland geometry to exclude any shadows.
|
||||
For example, GTK 4 pop-ups with pointing arrows (`has-arrow=true` property) are *not* rounded rectangles—the arrow sticks out—so if you enable blur, it will also stick out of the pop-up.
|
||||
|
||||
| Correct | Wrong |
|
||||
|-----------------------------------------------------|--------------------------------------------------------------------------------|
|
||||
| The pop-up is a rounded rectangle. Blur looks fine. | The pop-up is not a rounded rectangle. Blur extends above, where the arrow is. |
|
||||
|  |  |
|
||||
|
||||
These pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
|
||||
|
||||
#### Size Overrides
|
||||
|
||||
You can amend the window's minimum and maximum size in logical pixels.
|
||||
|
||||
@@ -74,7 +74,7 @@ Here are some design considerations for the window layout logic.
|
||||
|
||||
## Default config
|
||||
|
||||
The [default config](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
|
||||
The [default config](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
|
||||
Importantly, it is not a "suggested rice config"; we don't want to startle people with full-on rainbow borders and crazy shaders.
|
||||
|
||||
Since we're not a complete desktop environment (and don't have the contributor base to become one), we cannot provide a fully integrated experience—distro spins are better positioned to do this.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
niri's documentation files are found in `docs/wiki/` and should be viewable and browsable in at least three systems:
|
||||
|
||||
- The GitHub repo's markdown file preview
|
||||
- [The GitHub repo's wiki](https://github.com/YaLTeR/niri/wiki)
|
||||
- [The documentation site](https://yalter.github.io/niri/)
|
||||
- [The GitHub repo's wiki](https://github.com/niri-wm/niri/wiki)
|
||||
- [The documentation site](https://niri-wm.github.io/niri/)
|
||||
|
||||
## The GitHub repo's wiki
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
This is a checklist of things to release a new niri version.
|
||||
|
||||
We'll use `26.04` as the example new version.
|
||||
When making a patch release, append the patch number like `26.04.1`.
|
||||
|
||||
## Prepare the release notes
|
||||
|
||||
Plan for a few days of work, this usually takes a while.
|
||||
|
||||
During this process, also check:
|
||||
|
||||
- that all additions are marked with "next release" on the wiki,
|
||||
- if anything needs updating in `README.md`.
|
||||
|
||||
## Bump version
|
||||
|
||||
We use `year.month.patch` versioning.
|
||||
If the month contains a leading zero, drop it from the crate version (Cargo requirement).
|
||||
|
||||
You can use the command from [cargo-edit](https://github.com/killercup/cargo-edit):
|
||||
|
||||
```
|
||||
cargo set-version 26.4.0
|
||||
```
|
||||
|
||||
Then, manually update version in:
|
||||
|
||||
- `[package.metadata.generate-rpm]` in Cargo.toml
|
||||
- Dependency example in `niri-ipc/README.md`
|
||||
- Dependency example in `niri-ipc/src/lib.rs`
|
||||
|
||||
Do a full text search for the old version to make sure there are no other places.
|
||||
|
||||
## Replace all "Since: next release" mentions
|
||||
|
||||
Do a full text search for `next release`, replace everything with the new version number.
|
||||
|
||||
## Build, test, push, and have the CI run
|
||||
|
||||
Run all tests:
|
||||
|
||||
```
|
||||
RUN_SLOW_TESTS=1 cargo test --release --all
|
||||
```
|
||||
|
||||
- Run `cargo package -p niri-ipc` and make sure it succeeds.
|
||||
- Make sure the CI passes.
|
||||
- Make sure the niri-git COPR build passes.
|
||||
|
||||
## Trigger the "Prepare release" workflow on GitHub Actions
|
||||
|
||||
Set the "Public version" input to a version like `26.04`.
|
||||
|
||||
This workflow will:
|
||||
|
||||
- do some pre-release checks like grepping the wiki for "next version",
|
||||
- make a vendored dependency archive,
|
||||
- build and test niri with that dependency archive,
|
||||
- draft a new GitHub release with the archive attached.
|
||||
It will NOT override an existing draft release with the same name so the release notes are safe.
|
||||
|
||||
Make sure it succeeds and grab the vendored dependency archive that it produces.
|
||||
|
||||
## Update the niri COPR spec, update licenses in .spec.rpkg
|
||||
|
||||
You can grab the previous spec from [the last build](https://copr.fedorainfracloud.org/coprs/yalter/niri/builds/) in the COPR.
|
||||
|
||||
- Update version global to `26.04`.
|
||||
- Update commit global to the commit hash corresponding to the release commit.
|
||||
You can use `git rev-parse HEAD`.
|
||||
- Reset the `Release:` number to 1 if it was higher.
|
||||
|
||||
To run a test build, you can download the vendored dependency archive from the last step.
|
||||
Comment/uncomment `Source:` and `%autosetup` lines accordingly.
|
||||
|
||||
Download the source files:
|
||||
|
||||
```
|
||||
spectool -g niri.spec
|
||||
```
|
||||
|
||||
Build RPMs:
|
||||
|
||||
```
|
||||
fedpkg --release 44 mockbuild
|
||||
```
|
||||
|
||||
During the build, it will print the list of licenses.
|
||||
Update it in both the COPR spec and in `niri.spec.rpkg` accordingly.
|
||||
|
||||
If you had to update `niri.spec.rpkg` and therefore make another commit to the niri repo, make sure to update the commit hash in the COPR spec again.
|
||||
|
||||
Revert any temporary changes that you did to the COPR spec for local testing.
|
||||
|
||||
## Create and push the release git tag
|
||||
|
||||
The tag starts with a `v`:
|
||||
|
||||
```
|
||||
git tag -am "v26.04 release" v26.04
|
||||
git push origin v26.04
|
||||
```
|
||||
|
||||
While you can let GitHub create the tag automatically upon creating the release, this is not recommended.
|
||||
GitHub creates a *lightweight* tag, but we want an annotated tag that plays better with various tooling.
|
||||
|
||||
## Publish the release on GitHub
|
||||
|
||||
- Either upload the vendored dependencies file to your draft release with the release notes, or move the release notes to the GitHub-created release (the difference is that it's attributed to github-actions).
|
||||
- Set the tag to `v26.04`.
|
||||
- Set the release title to `v26.04`.
|
||||
- Check "Create a discussion for this release".
|
||||
|
||||
## Publish the niri-ipc crate
|
||||
|
||||
```
|
||||
cargo publish -p niri-ipc
|
||||
```
|
||||
|
||||
## Kick off the COPR build
|
||||
|
||||
Upload on the web or:
|
||||
|
||||
```
|
||||
copr-cli build niri niri.spec
|
||||
```
|
||||
|
||||
## Announce the release
|
||||
|
||||
Chat rooms, social media, etc.
|
||||
+19
-7
@@ -40,6 +40,20 @@ hotkey-overlay {
|
||||
}
|
||||
```
|
||||
|
||||
### How to fix lag on external monitors connected to a hybrid GPU laptop?
|
||||
|
||||
Hybrid GPU laptops (which have both an integrated and a discrete GPU) generally connect the external monitor port to the discrete GPU.
|
||||
Meanwhile, the built-in monitor is connected to the integrated GPU, and the integrated GPU is used for rendering by default.
|
||||
|
||||
This is good and expected because the integrated GPU uses significantly less battery compared to the discrete GPU.
|
||||
However, this means that niri has to render the external monitor contents on the integrated GPU, then copy them over to the discrete GPU for display.
|
||||
On some laptops this can cause lag and stuttering (it gets worse with monitor resolution and refresh rate).
|
||||
|
||||
If your laptop has a MUX switch—usually a GPU toggle in the UEFI settings—then you can switch it to use the discrete GPU, then niri will render on the discrete GPU, and the external monitor won't lag.
|
||||
Otherwise, you can try configuring niri to render on the discrete GPU via the [`render-drm-device`](./Configuration:-Debug-Options.md#render-drm-device) debug option.
|
||||
|
||||
Keep in mind that using the discrete GPU for rendering will make the laptop's battery deplete much faster.
|
||||
|
||||
### How to run X11 apps like Steam or Discord?
|
||||
|
||||
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
|
||||
@@ -66,14 +80,12 @@ I wouldn't be too surprised if, down the road, xwayland-satellite becomes the st
|
||||
|
||||
### Can I enable blur behind semitransparent windows?
|
||||
|
||||
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/54).
|
||||
|
||||
There's also [a PR](https://github.com/YaLTeR/niri/pull/1634) adding blur to niri which you can build and run manually.
|
||||
Keep in mind that it's an experimental implementation that may have problems and performance concerns.
|
||||
<sup>Since: 26.04</sup> Yes.
|
||||
See the [window effects](./Window-Effects.md) wiki page.
|
||||
|
||||
### Can I make a window sticky / pinned / always on top / appear on all workspaces?
|
||||
|
||||
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/932).
|
||||
Not yet, follow/upvote [this issue](https://github.com/niri-wm/niri/issues/932).
|
||||
|
||||
You can emulate this with a script that uses the niri IPC.
|
||||
For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature (`toggle-follow-mode`).
|
||||
@@ -82,7 +94,7 @@ For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature
|
||||
|
||||
Firefox seems to first open the Bitwarden window with a generic Firefox title, and only later change the window title to Bitwarden, so you can't effectively target it with an `open-floating` window rule.
|
||||
|
||||
You'll need to use a script, for example [this one](https://github.com/YaLTeR/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
|
||||
You'll need to use a script, for example [this one](https://github.com/niri-wm/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
|
||||
|
||||
### Can I open a window directly in the current column / in the same column as another window?
|
||||
|
||||
@@ -92,7 +104,7 @@ Listen to the event stream for a new window opening, then call an action like `c
|
||||
Adding this directly to niri is challenging:
|
||||
|
||||
- The act of "opening a window directly in some column" by itself is quite involved. Niri will have to compute the exact initial window size provided how other windows in a column would resize in response. This logic exists, but it isn't directly pluggable to the code computing a size for a new window. Then, it'll need to handle all sorts of edge cases like the column disappearing, or new windows getting added to the column, before the target window had a chance to appear.
|
||||
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/YaLTeR/niri/discussions/1125.
|
||||
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/niri-wm/niri/discussions/1125.
|
||||
|
||||
### Why does moving the mouse against a monitor edge focus the next window, but only sometimes?
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ You can make a window open in a maximized column with the [`open-maximized true`
|
||||
|
||||
<sup>Since: 25.11</sup>
|
||||
|
||||
You can maximize an individual window via `maximize-window-to-edges`.
|
||||
You can maximize an individual window via `maximize-window-to-edges` (bound to <kbd>Mod</kbd><kbd>M</kbd> by default).
|
||||
This is the same maximize as you can find on other desktop environments and operating systems: it expands a window to the edges of the available screen area.
|
||||
You will still see your bar, but not struts, gaps, or borders.
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ sudo dnf install niri dms
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
|
||||
Arch Linux:
|
||||
```
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
|
||||
paru -S dms-shell-bin matugen cava qt6-multimedia-ffmpeg
|
||||
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty dms-shell-niri matugen cava qt6-multimedia-ffmpeg
|
||||
systemctl --user add-wants niri.service dms
|
||||
```
|
||||
|
||||
@@ -147,13 +146,10 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the bottom window in the focused column into its own column |
|
||||
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
|
||||
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column heights |
|
||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
||||
| <kbd>Mod</kbd><kbd>R</kbd> and <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column widths forward and back |
|
||||
| <kbd>Mod</kbd><kbd>M</kbd> | Maximize window |
|
||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ To get a taste of the events, run `niri msg event-stream`.
|
||||
Though, this is more of a debug function than anything.
|
||||
You can get raw events from `niri msg --json event-stream`, or by connecting to the niri socket and requesting an event stream manually.
|
||||
|
||||
You can find the full list of events along with documentation [here](https://yalter.github.io/niri/niri_ipc/enum.Event.html).
|
||||
You can find the full list of events along with documentation [here](https://niri-wm.github.io/niri/niri_ipc/enum.Event.html).
|
||||
|
||||
### Programmatic Access
|
||||
|
||||
@@ -57,7 +57,7 @@ $ env NIRI_SOCKET=./temp.sock niri msg action focus-workspace 2
|
||||
{"Action":{"FocusWorkspace":{"reference":{"Index":2}}}}
|
||||
```
|
||||
|
||||
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://yalter.github.io/niri/niri_ipc/).
|
||||
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://niri-wm.github.io/niri/niri_ipc/).
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ First, for creating a niri package, see the [Packaging](./Packaging-niri.md) pag
|
||||
### Configuration
|
||||
|
||||
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
|
||||
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
|
||||
|
||||
This means that you can customize your distribution defaults by creating `/etc/niri/config.kdl`.
|
||||
When this file is present, niri *will not* automatically create a config at `~/.config/niri/`, so you'll need to direct your users how to do it themselves.
|
||||
|
||||
Keep in mind that we update the default config in new releases, so if you have a custom `/etc/niri/config.kdl`, you likely want to inspect and apply the relevant changes too.
|
||||
|
||||
You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
|
||||
The default configuration locations can be overridden with the `NIRI_CONFIG` environment variable.
|
||||
|
||||
<sup>Since: 26.04</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
|
||||
|
||||
<sup>Since: 25.11</sup> You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
|
||||
|
||||
### Xwayland
|
||||
|
||||
@@ -32,7 +36,7 @@ Make sure your system installer sets the keyboard layout via systemd-localed, an
|
||||
### Autostart
|
||||
|
||||
Niri works with the normal systemd autostart.
|
||||
The default [niri.service](https://github.com/YaLTeR/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
|
||||
The default [niri.service](https://github.com/niri-wm/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
|
||||
|
||||
To make a program run at niri startup without editing the niri config, you can either link its .desktop to `~/.config/autostart/`, or use a .service file with `WantedBy=graphical-session.target`.
|
||||
See the [example systemd setup](./Example-systemd-Setup.md) page for some examples.
|
||||
|
||||
@@ -47,7 +47,7 @@ Set them as automatically-installed optional dependencies, if possible.
|
||||
Working hardware acceleration is required for running niri.
|
||||
- Some notification daemon like `mako`, generally required for apps to work correctly.
|
||||
|
||||
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
|
||||
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
|
||||
|
||||
### Running tests
|
||||
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ Feel free to look through usage and [Getting started](./Getting-Started.md).
|
||||
If you're looking for ways to configure niri, check out the [introduction to configuration](./Configuration:-Introduction.md).
|
||||
|
||||
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
|
||||
See [CONTRIBUTING.md](https://github.com/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
|
||||
|
||||
If you're not already here, check out our new wiki website! https://yalter.github.io/niri/
|
||||
If you're not already here, check out our new wiki website! https://niri-wm.github.io/niri/
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
### Overview
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
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 Alacritty terminal.
|
||||
window-rule {
|
||||
match app-id="^Alacritty$"
|
||||
|
||||
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.
|
||||
|
||||
Windows can also blur their pop-up menus using `ext-background-effect`.
|
||||
On the niri side, you can do it with a `popups` block inside [`window-rule`](./Configuration:-Window-Rules.md#popups) and [`layer-rule`](./Configuration:-Layer-Rules.md#popups).
|
||||
See those wiki pages for examples and limitations.
|
||||
|
||||
Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur.
|
||||
|
||||
### Xray
|
||||
|
||||
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).
|
||||
|
||||
If you have an animated wallpaper, xray will still have to recompute blur every frame, but that happens once and shared among all windows, rather than recomputed separately for each window.
|
||||
|
||||
#### 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.
|
||||
|
||||
> [!WARNING]
|
||||
> 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 requires a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
|
||||
|
||||
### Implementation notes
|
||||
|
||||
The `ext-background-effect` protocol supports any wl_surface.
|
||||
We currently implement it only for toplevels, layer surfaces, and pop-ups, which should cover the vast majority of what's actually used by applications.
|
||||
|
||||
For pop-ups, effects default to *non-xray* because pop-ups generally appear on top of windows.
|
||||
|
||||
In particular, the following surface types don't support `ext-background-effect`.
|
||||
They can be implemented as the need arises.
|
||||
|
||||
- Subsurfaces. Would require implementing `clip-to-geometry` support for background effects.
|
||||
- Lock surfaces. Not useful as it would just show our red locked session background.
|
||||
- Cursor and drag-and-drop icon.
|
||||
The main challenge here will be screencasts where the cursor is rendered separately.
|
||||
This is problematic because non-xray effects require rendering the whole scene in one go rather than separately.
|
||||
@@ -47,7 +47,7 @@ It will open as a new window.
|
||||
|
||||
This method involves invoking XWayland directly and running it as its own window, it also requires an extra X11 window manager running inside it.
|
||||
|
||||

|
||||

|
||||
|
||||
Here's how you do it:
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -41,6 +42,7 @@
|
||||
* [Design Principles](./Development:-Design-Principles.md)
|
||||
* [Developing niri](./Development:-Developing-niri.md)
|
||||
* [Documenting niri](./Development:-Documenting-niri.md)
|
||||
* [Releasing niri](./Development:-Releasing-niri.md)
|
||||
* [Fractional Layout](./Development:-Fractional-Layout.md)
|
||||
* [Redraw Loop](./Development:-Redraw-Loop.md)
|
||||
* [Animation Timing](./Development:-Animation-Timing.md)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:44cbf3144d7add741762a8034779602f6a94b2b99a6d62774c9d75f3606b98b1
|
||||
size 1582702
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5a63ea3cc2f158e175c00dd058988a2bbf676e2a2aac5c2ef1603bd983589d5
|
||||
size 166777
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bef0c57d617916bf6014fe08e268c8201d7f6ef682e3aea3395e76116b1d0400
|
||||
size 56936
|
||||
@@ -20,6 +20,7 @@
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
revision = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
@@ -46,7 +47,7 @@
|
||||
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "niri";
|
||||
version = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
version = revision;
|
||||
|
||||
src = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
@@ -107,7 +108,7 @@
|
||||
buildNoDefaultFeatures = true;
|
||||
|
||||
# ever since this commit:
|
||||
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
|
||||
# https://github.com/niri-wm/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
|
||||
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
|
||||
# and thus creates a real socket file in the runtime dir.
|
||||
# this is fine for our build, we just need to make sure it has a directory to write to.
|
||||
@@ -148,7 +149,7 @@
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
NIRI_BUILD_COMMIT = self.shortRev;
|
||||
NIRI_BUILD_COMMIT = revision;
|
||||
};
|
||||
|
||||
passthru = {
|
||||
@@ -157,7 +158,7 @@
|
||||
|
||||
meta = {
|
||||
description = "Scrollable-tiling Wayland compositor";
|
||||
homepage = "https://github.com/YaLTeR/niri";
|
||||
homepage = "https://github.com/niri-wm/niri";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "niri";
|
||||
platforms = lib.platforms.linux;
|
||||
|
||||
@@ -9,11 +9,11 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.8.1"
|
||||
csscolorparser = "0.8.3"
|
||||
knuffel = "3.2.0"
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.11.0", path = "../niri-ipc" }
|
||||
regex = "1.12.2"
|
||||
niri-ipc = { version = "26.4.0", path = "../niri-ipc" }
|
||||
regex = "1.12.3"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
@@ -1006,6 +1006,103 @@ 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,
|
||||
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<u8>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub offset: Option<FloatOrInt<0, 100>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub noise: Option<FloatOrInt<0, 1000>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub saturation: Option<FloatOrInt<0, 1000>>,
|
||||
}
|
||||
|
||||
impl MergeWith<BlurPart> 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<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub blur: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub noise: Option<FloatOrInt<0, 1000>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub saturation: Option<FloatOrInt<0, 1000>>,
|
||||
}
|
||||
|
||||
/// 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<bool>,
|
||||
|
||||
/// 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<bool>,
|
||||
|
||||
pub noise: Option<f64>,
|
||||
pub saturation: Option<f64>,
|
||||
}
|
||||
|
||||
impl MergeWith<BackgroundEffectRule> 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};
|
||||
|
||||
+26
-38
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -368,7 +369,7 @@ pub enum Action {
|
||||
#[knuffel(skip)]
|
||||
UnsetWindowUrgent(u64),
|
||||
#[knuffel(skip)]
|
||||
LoadConfigFile,
|
||||
LoadConfigFile(#[knuffel(argument)] Option<String>),
|
||||
#[knuffel(skip)]
|
||||
MruAdvance {
|
||||
direction: MruDirection,
|
||||
@@ -699,7 +700,7 @@ impl From<niri_ipc::Action> for Action {
|
||||
niri_ipc::Action::ToggleWindowUrgent { id } => Self::ToggleWindowUrgent(id),
|
||||
niri_ipc::Action::SetWindowUrgent { id } => Self::SetWindowUrgent(id),
|
||||
niri_ipc::Action::UnsetWindowUrgent { id } => Self::UnsetWindowUrgent(id),
|
||||
niri_ipc::Action::LoadConfigFile {} => Self::LoadConfigFile,
|
||||
niri_ipc::Action::LoadConfigFile { path } => Self::LoadConfigFile(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,7 +770,7 @@ where
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
expect_only_children(node, ctx);
|
||||
|
||||
let mut seen_keys = HashSet::new();
|
||||
let mut seen_keys: HashMap<Key, &knuffel::ast::SpannedNode<S>> = HashMap::new();
|
||||
|
||||
let mut binds = Vec::new();
|
||||
|
||||
@@ -779,39 +780,26 @@ where
|
||||
ctx.emit_error(e);
|
||||
}
|
||||
Ok(bind) => {
|
||||
if seen_keys.insert(bind.key) {
|
||||
binds.push(bind);
|
||||
} else {
|
||||
// ideally, this error should point to the previous instance of this keybind
|
||||
//
|
||||
// i (sodiboo) have tried to implement this in various ways:
|
||||
// miette!(), #[derive(Diagnostic)]
|
||||
// DecodeError::Custom, DecodeError::Conversion
|
||||
// nothing seems to work, and i suspect it's not possible.
|
||||
//
|
||||
// DecodeError is fairly restrictive.
|
||||
// even DecodeError::Custom just wraps a std::error::Error
|
||||
// and this erases all rich information from miette. (why???)
|
||||
//
|
||||
// why does knuffel do this?
|
||||
// from what i can tell, it doesn't even use DecodeError for much.
|
||||
// it only ever converts them to a Report anyways!
|
||||
// https://github.com/tailhook/knuffel/blob/c44c6b0c0f31ea6d1174d5d2ed41064922ea44ca/src/wrappers.rs#L55-L58
|
||||
//
|
||||
// besides like, allowing downstream users (such as us!)
|
||||
// to match on parse failure, i don't understand why
|
||||
// it doesn't just use a generic error type
|
||||
//
|
||||
// even the matching isn't consistent,
|
||||
// because errors can also be omitted as ctx.emit_error.
|
||||
// why does *that one* especially, require a DecodeError?
|
||||
//
|
||||
// anyways if you can make it format nicely, definitely do fix this
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&child.node_name,
|
||||
"keybind",
|
||||
"duplicate keybind",
|
||||
));
|
||||
match seen_keys.entry(bind.key) {
|
||||
Entry::Occupied(entry) => {
|
||||
// Even though it's technically incorrect, we use
|
||||
// `DecodeError::Missing` here because it labels the bind with
|
||||
// "node starts here", which is the least bad option
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
entry.get(),
|
||||
"keybind first defined here",
|
||||
));
|
||||
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&child.node_name,
|
||||
"keybind",
|
||||
"duplicate keybind later defined here",
|
||||
));
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(child);
|
||||
binds.push(bind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1034,7 +1022,7 @@ impl FromStr for Key {
|
||||
// [0]: https://github.com/xkbcommon/libxkbcommon/blob/45a118d5325b051343b4b174f60c1434196fa7d4/src/keysym.c#L276
|
||||
// [1]: https://docs.rs/xkbcommon/latest/xkbcommon/xkb/keysyms/index.html#:~:text=KEY%5FXF86ScreenSaver
|
||||
//
|
||||
// See https://github.com/YaLTeR/niri/issues/1969
|
||||
// See https://github.com/niri-wm/niri/issues/1969
|
||||
if keysym == Keysym::XF86_Screensaver {
|
||||
keysym = keysym_from_name(key, KEYSYM_NO_FLAGS);
|
||||
if keysym.raw() == KEY_NoSymbol {
|
||||
|
||||
@@ -364,6 +364,8 @@ pub struct Tablet {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub map_to_output: Option<String>,
|
||||
#[knuffel(child)]
|
||||
pub map_to_focused_output: bool,
|
||||
#[knuffel(child)]
|
||||
pub left_handed: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use crate::utils::RegexEq;
|
||||
use crate::window_rule::PopupsRule;
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LayerRule {
|
||||
@@ -20,6 +21,10 @@ pub struct LayerRule {
|
||||
pub place_within_backdrop: Option<bool>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub baba_is_float: Option<bool>,
|
||||
#[knuffel(child, default)]
|
||||
pub background_effect: BackgroundEffectRule,
|
||||
#[knuffel(child, default)]
|
||||
pub popups: PopupsRule,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
@@ -28,4 +33,6 @@ pub struct Match {
|
||||
pub namespace: Option<RegexEq>,
|
||||
#[knuffel(property)]
|
||||
pub at_startup: Option<bool>,
|
||||
#[knuffel(property, str)]
|
||||
pub layer: Option<niri_ipc::Layer>,
|
||||
}
|
||||
|
||||
+64
-4
@@ -59,7 +59,9 @@ use crate::recent_windows::RecentWindowsPart;
|
||||
pub use crate::recent_windows::{MruDirection, MruFilter, MruPreviews, MruScope, RecentWindows};
|
||||
pub use crate::utils::FloatOrInt;
|
||||
use crate::utils::{Flag, MergeWith as _};
|
||||
pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule};
|
||||
pub use crate::window_rule::{
|
||||
FloatingPosition, PopupsRule, RelativeTo, ResolvedPopupsRules, WindowRule,
|
||||
};
|
||||
pub use crate::workspace::{Workspace, WorkspaceLayoutPart};
|
||||
|
||||
const RECURSION_LIMIT: u8 = 10;
|
||||
@@ -78,6 +80,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 +197,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),
|
||||
@@ -336,12 +340,26 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
let path = base.0.join(path);
|
||||
|
||||
// We use DecodeError::Missing throughout this block because it results in the
|
||||
// least confusing error messages while still allowing to provide a span.
|
||||
|
||||
// Expand ~ into the home dir
|
||||
let path = if let Ok(rest) = path.strip_prefix("~") {
|
||||
let Some(home) = std::env::home_dir() else {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("error retrieving home directory to expand {path:?}"),
|
||||
));
|
||||
continue;
|
||||
};
|
||||
|
||||
home.join(rest)
|
||||
} else {
|
||||
// Otherwise, use the current include base dir
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
base.0.join(path)
|
||||
};
|
||||
|
||||
let recursion = ctx.get::<Recursion>().unwrap().0 + 1;
|
||||
if recursion == RECURSION_LIMIT {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
@@ -701,6 +719,7 @@ mod tests {
|
||||
|
||||
tablet {
|
||||
map-to-output "eDP-1"
|
||||
map-to-focused-output
|
||||
calibration-matrix 1.0 2.0 3.0 \
|
||||
4.0 5.0 6.0
|
||||
}
|
||||
@@ -1093,6 +1112,7 @@ mod tests {
|
||||
map_to_output: Some(
|
||||
"eDP-1",
|
||||
),
|
||||
map_to_focused_output: true,
|
||||
left_handed: false,
|
||||
},
|
||||
touch: Touch {
|
||||
@@ -1616,6 +1636,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 +1872,22 @@ mod tests {
|
||||
),
|
||||
scroll_factor: None,
|
||||
tiled_state: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
popups: PopupsRule {
|
||||
opacity: None,
|
||||
geometry_corner_radius: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
layer_rules: [
|
||||
@@ -1859,6 +1902,7 @@ mod tests {
|
||||
),
|
||||
),
|
||||
at_startup: None,
|
||||
layer: None,
|
||||
},
|
||||
],
|
||||
excludes: [],
|
||||
@@ -1879,6 +1923,22 @@ 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,
|
||||
},
|
||||
popups: PopupsRule {
|
||||
opacity: None,
|
||||
geometry_corner_radius: None,
|
||||
background_effect: BackgroundEffectRule {
|
||||
xray: None,
|
||||
blur: None,
|
||||
noise: None,
|
||||
saturation: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
binds: Binds(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use niri_ipc::ColumnDisplay;
|
||||
|
||||
use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
|
||||
use crate::appearance::{
|
||||
BackgroundEffect, BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule,
|
||||
TabIndicatorRule,
|
||||
};
|
||||
use crate::layout::DefaultPresetSize;
|
||||
use crate::utils::RegexEq;
|
||||
use crate::utils::{MergeWith, RegexEq};
|
||||
use crate::FloatOrInt;
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
@@ -72,6 +75,46 @@ pub struct WindowRule {
|
||||
pub scroll_factor: Option<FloatOrInt<0, 100>>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub tiled_state: Option<bool>,
|
||||
#[knuffel(child, default)]
|
||||
pub background_effect: BackgroundEffectRule,
|
||||
#[knuffel(child, default)]
|
||||
pub popups: PopupsRule,
|
||||
}
|
||||
|
||||
/// Rules for popup surfaces.
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct PopupsRule {
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub opacity: Option<f32>,
|
||||
#[knuffel(child)]
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
#[knuffel(child, default)]
|
||||
pub background_effect: BackgroundEffectRule,
|
||||
}
|
||||
|
||||
/// Resolved popup-specific rules.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct ResolvedPopupsRules {
|
||||
/// Extra opacity to draw popups with.
|
||||
pub opacity: Option<f32>,
|
||||
|
||||
/// Corner radius to assume the popups have.
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
|
||||
/// Background effect configuration for popups.
|
||||
pub background_effect: BackgroundEffect,
|
||||
}
|
||||
|
||||
impl MergeWith<PopupsRule> for ResolvedPopupsRules {
|
||||
fn merge_with(&mut self, part: &PopupsRule) {
|
||||
if let Some(x) = part.opacity {
|
||||
self.opacity = Some(x);
|
||||
}
|
||||
if let Some(x) = part.geometry_corner_radius {
|
||||
self.geometry_corner_radius = Some(x);
|
||||
}
|
||||
self.background_effect.merge_with(&part.background_effect);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "1.2.0", optional = true }
|
||||
schemars = { version = "1.2.1", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# niri-ipc
|
||||
|
||||
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
|
||||
Types and helpers for interfacing with the [niri](https://github.com/niri-wm/niri) Wayland compositor.
|
||||
|
||||
## Backwards compatibility
|
||||
|
||||
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.11.0"
|
||||
niri-ipc = "=26.4.0"
|
||||
```
|
||||
|
||||
+22
-2
@@ -41,7 +41,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.11.0"
|
||||
//! niri-ipc = "=26.4.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -936,7 +936,13 @@ pub enum Action {
|
||||
///
|
||||
/// Can be useful for scripts changing the config file, to avoid waiting the small duration for
|
||||
/// niri's config file watcher to notice the changes.
|
||||
LoadConfigFile {},
|
||||
LoadConfigFile {
|
||||
/// Path of a new config file to load.
|
||||
///
|
||||
/// If unset, reloads the current config file.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
@@ -1862,6 +1868,20 @@ impl FromStr for Transform {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Layer {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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;
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ repository.workspace = true
|
||||
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.11.0", path = ".." }
|
||||
niri-config = { version = "25.11.0", path = "../niri-config" }
|
||||
niri = { version = "26.4.0", path = ".." }
|
||||
niri-config = { version = "26.4.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::layout::Options;
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri::render_helpers::xray::XrayPos;
|
||||
use niri::render_helpers::{RenderCtx, RenderTarget};
|
||||
use niri_config::Color;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -121,13 +122,16 @@ 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,
|
||||
};
|
||||
let xray_pos = XrayPos::new(location, 1.);
|
||||
self.tile
|
||||
.render(ctx, location, xray_pos, true, &mut |elem| {
|
||||
rv.push(Box::new(elem) as _)
|
||||
});
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ mod imp {
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
|
||||
use smithay::reexports::gbm::Format as Fourcc;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
@@ -206,8 +207,15 @@ mod imp {
|
||||
|
||||
if let Some(mut damage) = rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
|
||||
let cache = UserDataMap::new();
|
||||
if element.is_framebuffer_effect() {
|
||||
element
|
||||
.capture_framebuffer(&mut frame, src, dst, &cache)
|
||||
.context("error in capture_framebuffer()")?;
|
||||
}
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage], &[])
|
||||
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
_ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
_target: RenderTarget,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
+2
-4
@@ -38,7 +38,6 @@ SourceLicense: GPL-3.0-or-later
|
||||
# 0BSD OR MIT OR Apache-2.0
|
||||
# Apache-2.0
|
||||
# Apache-2.0 AND MIT
|
||||
# Apache-2.0 OR BSL-1.0
|
||||
# Apache-2.0 OR MIT
|
||||
# Apache-2.0 OR MIT OR Unlicense
|
||||
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
||||
@@ -53,14 +52,13 @@ SourceLicense: GPL-3.0-or-later
|
||||
# MIT OR Apache-2.0 OR Zlib
|
||||
# MIT OR Zlib OR Apache-2.0
|
||||
# MPL-2.0
|
||||
# Unicode-3.0
|
||||
# Unlicense OR MIT
|
||||
# Zlib
|
||||
# Zlib OR Apache-2.0 OR MIT
|
||||
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib) AND (Zlib OR Apache-2.0 OR MIT)
|
||||
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND Apache-2.0 AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND BSD-2-Clause AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND GPL-3.0-or-later AND ISC AND MIT AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND MPL-2.0 AND (Unlicense OR MIT) AND Zlib AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
URL: https://github.com/niri-wm/niri
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// This config is in the KDL format: https://kdl.dev
|
||||
// "/-" comments out the following node.
|
||||
// Check the wiki for a full description of the configuration:
|
||||
// https://yalter.github.io/niri/Configuration:-Introduction
|
||||
// https://niri-wm.github.io/niri/Configuration:-Introduction
|
||||
|
||||
// Input device configuration.
|
||||
// Find the full list of options on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Input
|
||||
// https://niri-wm.github.io/niri/Configuration:-Input
|
||||
input {
|
||||
keyboard {
|
||||
xkb {
|
||||
@@ -73,7 +73,7 @@ input {
|
||||
// by running `niri msg outputs` while inside a niri instance.
|
||||
// The built-in laptop monitor is usually called "eDP-1".
|
||||
// Find more information on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Outputs
|
||||
// https://niri-wm.github.io/niri/Configuration:-Outputs
|
||||
// Remember to uncomment the node by removing "/-"!
|
||||
/-output "eDP-1" {
|
||||
// Uncomment this line to disable this output.
|
||||
@@ -108,7 +108,7 @@ input {
|
||||
|
||||
// Settings that influence how windows are positioned and sized.
|
||||
// Find more information on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Layout
|
||||
// https://niri-wm.github.io/niri/Configuration:-Layout
|
||||
layout {
|
||||
// Set gaps around windows in logical pixels.
|
||||
gaps 16
|
||||
@@ -134,7 +134,7 @@ layout {
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
@@ -295,7 +295,7 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||
|
||||
// Animation settings.
|
||||
// The wiki explains how to configure individual animations:
|
||||
// https://yalter.github.io/niri/Configuration:-Animations
|
||||
// https://niri-wm.github.io/niri/Configuration:-Animations
|
||||
animations {
|
||||
// Uncomment to turn off all animations.
|
||||
// off
|
||||
@@ -306,7 +306,7 @@ animations {
|
||||
|
||||
// Window rules let you adjust behavior for individual windows.
|
||||
// Find more information on the wiki:
|
||||
// https://yalter.github.io/niri/Configuration:-Window-Rules
|
||||
// https://niri-wm.github.io/niri/Configuration:-Window-Rules
|
||||
|
||||
// Work around WezTerm's initial configure bug
|
||||
// by setting an empty default-column-width.
|
||||
@@ -550,11 +550,14 @@ binds {
|
||||
// Expel the bottom window from the focused column to the right.
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// Cycle through widths set in preset-column-widths.
|
||||
Mod+R { switch-preset-column-width; }
|
||||
// Cycling through the presets in reverse order is also possible.
|
||||
// Mod+R { switch-preset-column-width-back; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Shift+R { switch-preset-column-width-back; }
|
||||
|
||||
Mod+Ctrl+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
|
||||
+89
-39
@@ -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] = [
|
||||
@@ -97,9 +97,6 @@ pub struct Tty {
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
// The output config had changed, but the session is paused, so we need to update it on resume.
|
||||
update_output_config_on_resume: bool,
|
||||
// The ignored nodes have changed, but the session is paused, so we need to update it on
|
||||
// resume.
|
||||
update_ignored_nodes_on_resume: bool,
|
||||
// Whether the debug tinting is enabled.
|
||||
debug_tint: bool,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -441,6 +438,14 @@ impl Tty {
|
||||
}
|
||||
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
|
||||
|
||||
// If the session is not active at startup (e.g. niri was launched from a different TTY),
|
||||
// suspend libinput now so that when ActivateSession fires, libinput.resume() performs a
|
||||
// full re-enumeration of input devices instead of being a no-op.
|
||||
if !session.is_active() {
|
||||
debug!("session is not active, starting libinput in paused state");
|
||||
libinput.suspend();
|
||||
}
|
||||
|
||||
let input_backend = LibinputInputBackend::new(libinput.clone());
|
||||
event_loop
|
||||
.insert_source(input_backend, |mut event, _, state| {
|
||||
@@ -487,11 +492,6 @@ impl Tty {
|
||||
}
|
||||
info!("using as the render node: {node_path}");
|
||||
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&config.borrow());
|
||||
if ignored_nodes.remove(&primary_node) || ignored_nodes.remove(&primary_render_node) {
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
session,
|
||||
@@ -500,17 +500,27 @@ impl Tty {
|
||||
gpu_manager,
|
||||
primary_node,
|
||||
primary_render_node,
|
||||
ignored_nodes,
|
||||
ignored_nodes: HashSet::new(),
|
||||
devices: HashMap::new(),
|
||||
dmabuf_global: None,
|
||||
update_output_config_on_resume: false,
|
||||
update_ignored_nodes_on_resume: false,
|
||||
debug_tint: false,
|
||||
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
// If the session is inactive, skip initialization because we won't be able to do much with
|
||||
// the devices anyway. We'll get ActivateSession and add the devices there instead.
|
||||
//
|
||||
// This can happen when starting niri while having a different TTY active (e.g. via tmux).
|
||||
if !self.session.is_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the ignored nodes.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
let udev = self.udev_dispatcher.clone();
|
||||
let udev = udev.as_source_ref();
|
||||
|
||||
@@ -550,6 +560,10 @@ impl Tty {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recompute ignored nodes to resolve symlinks (like /dev/dri/by-path/...) to their
|
||||
// new underlying device IDs.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
if let Err(err) = self.device_added(device_id, &path, niri) {
|
||||
warn!("error adding device: {err:?}");
|
||||
}
|
||||
@@ -597,16 +611,9 @@ impl Tty {
|
||||
warn!("error resuming libinput");
|
||||
}
|
||||
|
||||
if self.update_ignored_nodes_on_resume {
|
||||
self.update_ignored_nodes_on_resume = false;
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
||||
if ignored_nodes.remove(&self.primary_node)
|
||||
|| ignored_nodes.remove(&self.primary_render_node)
|
||||
{
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
self.ignored_nodes = ignored_nodes;
|
||||
}
|
||||
// While the session was suspended, GPUs could have been added, so
|
||||
// /dev/dri/by-path/... symlinks need to be re-resolved.
|
||||
self.ignored_nodes = self.compute_ignored_nodes();
|
||||
|
||||
let mut device_list = self
|
||||
.udev_dispatcher
|
||||
@@ -649,7 +656,7 @@ impl Tty {
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
|
||||
// Someone on an old device hit what seems to be a driver bug without this:
|
||||
// https://github.com/YaLTeR/niri/issues/3048
|
||||
// https://github.com/niri-wm/niri/issues/3048
|
||||
let force_disable = self
|
||||
.config
|
||||
.borrow()
|
||||
@@ -699,7 +706,14 @@ impl Tty {
|
||||
}
|
||||
|
||||
// Add new devices.
|
||||
for (device_id, path) in device_list.into_iter() {
|
||||
//
|
||||
// Add the primary node first as later nodes might depend on the primary render
|
||||
// node being available.
|
||||
let primary_device_id = self.primary_node.dev_id();
|
||||
let primary_device_path = device_list.remove(&primary_device_id);
|
||||
let primary = primary_device_path.map(|path| (primary_device_id, path));
|
||||
|
||||
for (device_id, path) in primary.into_iter().chain(device_list) {
|
||||
if let Err(err) = self.device_added(device_id, &path, niri) {
|
||||
warn!("error adding device: {err:?}");
|
||||
}
|
||||
@@ -809,7 +823,10 @@ impl Tty {
|
||||
.context("error creating renderer")?;
|
||||
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
warn!("error binding wl-display in EGL: {err:?}");
|
||||
// wl_drm is on its way out so this is expected on most modern distros.
|
||||
trace!("error binding legacy EGL to wl_display: {err}");
|
||||
} else {
|
||||
debug!("bound legacy EGL to wl_display");
|
||||
}
|
||||
|
||||
let gles_renderer = renderer.as_gles_renderer();
|
||||
@@ -979,6 +996,32 @@ impl Tty {
|
||||
} => {
|
||||
removed.push(crtc);
|
||||
}
|
||||
// Emitted when the list of connector modes changes at runtime.
|
||||
//
|
||||
// Some devices, notably USB-C docks with DP-MST/alt-mode, report Connected before
|
||||
// the EDID has been read, with an empty mode list. Then, at a later point, the
|
||||
// modes will be populated, at which point we'll get this Changed event.
|
||||
DrmScanEvent::Changed {
|
||||
connector,
|
||||
crtc: Some(crtc),
|
||||
} => {
|
||||
let connector_name = format_connector_name(&connector);
|
||||
let name = make_output_name(&device.drm, connector.handle(), connector_name);
|
||||
debug!(
|
||||
"connector changed: {} \"{}\"",
|
||||
&name.connector,
|
||||
name.format_make_model_serial(),
|
||||
);
|
||||
|
||||
if !device.known_crtcs.contains_key(&crtc) {
|
||||
// I guess this can happen if the connector initially wasn't mapped to a
|
||||
// CRTC but then got mapped before being changed.
|
||||
warn!("changed connector missing from known crtcs");
|
||||
}
|
||||
|
||||
// We don't actually need to do anything here; on_output_config_changed() will
|
||||
// take care of picking a new mode if needed.
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1393,7 +1436,7 @@ impl Tty {
|
||||
|
||||
// Create the compositor.
|
||||
let res = DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -1423,7 +1466,7 @@ impl Tty {
|
||||
.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
|
||||
DrmCompositor::new(
|
||||
OutputModeSource::Auto(output.clone()),
|
||||
OutputModeSource::Auto(output.downgrade()),
|
||||
surface,
|
||||
None,
|
||||
device.allocator.clone(),
|
||||
@@ -1677,8 +1720,8 @@ impl Tty {
|
||||
// This is an error!() because it shouldn't happen, but on some systems it somehow
|
||||
// does. Kernel sending rogue vblank events?
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/556
|
||||
// https://github.com/YaLTeR/niri/issues/615
|
||||
// https://github.com/niri-wm/niri/issues/556
|
||||
// https://github.com/niri-wm/niri/issues/615
|
||||
error!(
|
||||
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
|
||||
can happen when resuming from sleep or powering on monitors: {state:?}"
|
||||
@@ -1839,8 +1882,12 @@ impl Tty {
|
||||
};
|
||||
|
||||
// Render the elements.
|
||||
let mut elements =
|
||||
niri.render::<TtyRenderer>(&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 {
|
||||
@@ -2236,22 +2283,25 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
||||
|
||||
// If we're inactive, we can't do anything, so just set a flag for later.
|
||||
if !self.session.is_active() {
|
||||
self.update_ignored_nodes_on_resume = true;
|
||||
return;
|
||||
}
|
||||
|
||||
fn compute_ignored_nodes(&self) -> HashSet<DrmNode> {
|
||||
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
||||
if ignored_nodes.remove(&self.primary_node)
|
||||
|| ignored_nodes.remove(&self.primary_render_node)
|
||||
{
|
||||
warn!("ignoring the primary node or render node is not allowed");
|
||||
}
|
||||
ignored_nodes
|
||||
}
|
||||
|
||||
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
||||
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
||||
|
||||
// If we're inactive, we can't do anything, but we'll recompute in ActivateSession.
|
||||
if !self.session.is_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ignored_nodes = self.compute_ignored_nodes();
|
||||
if ignored_nodes == self.ignored_nodes {
|
||||
return;
|
||||
}
|
||||
|
||||
+54
-9
@@ -4,8 +4,10 @@ use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use niri_config::{Config, OutputName};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::egl::EGLDevice;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||
@@ -14,13 +16,15 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||
use smithay::reexports::calloop::LoopHandle;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::platform::wayland::WindowAttributesExtWayland;
|
||||
use smithay::reexports::winit::window::Window;
|
||||
use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal};
|
||||
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 {
|
||||
@@ -28,6 +32,7 @@ pub struct Winit {
|
||||
output: Output,
|
||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
dmabuf_global: Option<DmabufGlobal>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
@@ -41,7 +46,8 @@ impl Winit {
|
||||
let builder = Window::default_attributes()
|
||||
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
||||
// .with_resizable(false)
|
||||
.with_title("niri");
|
||||
.with_title("niri")
|
||||
.with_name("niri", "");
|
||||
let (backend, winit) = winit::init_from_attributes(builder)?;
|
||||
|
||||
let output = Output::new(
|
||||
@@ -135,6 +141,7 @@ impl Winit {
|
||||
output,
|
||||
backend,
|
||||
damage_tracker,
|
||||
dmabuf_global: None,
|
||||
ipc_outputs,
|
||||
})
|
||||
}
|
||||
@@ -142,7 +149,10 @@ impl Winit {
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
let renderer = self.backend.renderer();
|
||||
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||
warn!("error binding renderer wl_display: {err}");
|
||||
// wl_drm is on its way out so this is expected on most modern distros.
|
||||
trace!("error binding legacy EGL to wl_display: {err}");
|
||||
} else {
|
||||
debug!("bound legacy EGL to wl_display");
|
||||
}
|
||||
|
||||
resources::init(renderer);
|
||||
@@ -162,9 +172,44 @@ impl Winit {
|
||||
|
||||
niri.update_shaders();
|
||||
|
||||
self.create_dmabuf_global(niri);
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
}
|
||||
|
||||
pub fn create_dmabuf_global(&mut self, niri: &mut Niri) {
|
||||
let renderer = self.backend.renderer();
|
||||
|
||||
let default_feedback = || {
|
||||
let display = renderer.egl_context().display();
|
||||
let device =
|
||||
EGLDevice::device_for_display(display).context("error getting EGL device")?;
|
||||
let node = device
|
||||
.try_get_render_node()
|
||||
.context("error getting EGL device render node")?
|
||||
.context("failed to query EGL device render node")?;
|
||||
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
DmabufFeedbackBuilder::new(node.dev_id(), primary_formats)
|
||||
.build()
|
||||
.context("error building dmabuf feedback")
|
||||
};
|
||||
|
||||
// Fallback to dmabuf v3 if we failed to build feedback.
|
||||
let dmabuf_global = match default_feedback() {
|
||||
Ok(feedback) => niri
|
||||
.dmabuf_state
|
||||
.create_global_with_default_feedback::<State>(&niri.display_handle, &feedback),
|
||||
Err(err) => {
|
||||
debug!("failed building default dmabuf feedback, falling back to v3: {err:?}");
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
niri.dmabuf_state
|
||||
.create_global::<State>(&niri.display_handle, primary_formats)
|
||||
}
|
||||
};
|
||||
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
|
||||
}
|
||||
|
||||
pub fn seat_name(&self) -> String {
|
||||
"winit".to_owned()
|
||||
}
|
||||
@@ -180,12 +225,12 @@ impl Winit {
|
||||
let _span = tracy_client::span!("Winit::render");
|
||||
|
||||
// Render the elements.
|
||||
let mut elements = niri.render::<GlesRenderer>(
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use smithay::delegate_background_effect;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Rectangle};
|
||||
use smithay::wayland::background_effect::{
|
||||
self, BackgroundEffectSurfaceCachedState, ExtBackgroundEffectHandler,
|
||||
};
|
||||
use smithay::wayland::compositor::{
|
||||
add_post_commit_hook, with_states, RegionAttributes, SurfaceData,
|
||||
};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::utils::region::region_to_non_overlapping_rects;
|
||||
|
||||
/// Per-surface cache for processed blur region (non-overlapping rects).
|
||||
#[derive(Default)]
|
||||
struct CachedBlurRegionUserData(Mutex<CachedBlurRegionInner>);
|
||||
|
||||
#[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<Arc<Vec<Rectangle<i32, Logical>>>>,
|
||||
}
|
||||
|
||||
/// Gets the cached blur region for a surface, lazily recomputing if dirty.
|
||||
pub fn get_cached_blur_region(states: &SurfaceData) -> Option<Arc<Vec<Rectangle<i32, Logical>>>> {
|
||||
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);
|
||||
|
||||
if cached.has::<BackgroundEffectSurfaceCachedState>() {
|
||||
let mut guard = cached.get::<BackgroundEffectSurfaceCachedState>();
|
||||
if let Some(region) = &guard.current().blur_region {
|
||||
region_to_non_overlapping_rects(region, rects);
|
||||
} else {
|
||||
inner.rects = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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::<State, _>(wl_surface, |_state, _dh, surface| {
|
||||
with_states(surface, |states| {
|
||||
if let Some(cache) = states.data_map.get::<CachedBlurRegionUserData>() {
|
||||
let mut guard = cache.0.lock().unwrap();
|
||||
if guard.pending_dirty {
|
||||
guard.pending_dirty = false;
|
||||
guard.dirty = true;
|
||||
|
||||
crate::render_helpers::background_effect::damage_surface(states);
|
||||
}
|
||||
} 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);
|
||||
@@ -195,7 +195,10 @@ impl CompositorHandler for State {
|
||||
// The mapped pre-commit hook deals with dma-bufs on its own.
|
||||
self.remove_default_dmabuf_pre_commit_hook(surface);
|
||||
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
|
||||
let mapped = Mapped::new(window, rules, hook);
|
||||
let mapped = {
|
||||
let config = self.niri.config.borrow();
|
||||
Mapped::new(window, rules, hook, &config)
|
||||
};
|
||||
let window = mapped.window.clone();
|
||||
|
||||
let target = if let Some(p) = &parent {
|
||||
@@ -486,11 +489,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +500,16 @@ impl CompositorHandler for State {
|
||||
.root_surface
|
||||
.retain(|k, v| k != surface && v != surface);
|
||||
|
||||
self.niri.dmabuf_pre_commit_hook.remove(surface);
|
||||
// The object destruction order is not guaranteed to follow the logical role order. So for
|
||||
// example when a client disconnects unexpectedly, WlSurface::destroyed() may be called
|
||||
// before XdgShellHandler::toplevel_destroyed(). In this case, the surface will *not* have
|
||||
// the default dmabuf pre-commit hook: it will still have the toplevel pre-commit hook.
|
||||
//
|
||||
// So, this may come out empty, and then the toplevel pre-commit hook will be removed in the
|
||||
// subsequent toplevel_destroyed() call.
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,6 +528,11 @@ delegate_shm!(State);
|
||||
|
||||
impl State {
|
||||
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if !surface.is_alive() {
|
||||
error!("tried to add dmabuf pre-commit hook for a dead surface");
|
||||
return;
|
||||
}
|
||||
|
||||
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
@@ -556,13 +572,13 @@ impl State {
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
remove_pre_commit_hook(surface, &prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
remove_pre_commit_hook(surface, &hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::compositor::{add_pre_commit_hook, get_parent, with_states, HookId};
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
WlrLayerShellState,
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceCachedState, LayerSurfaceData,
|
||||
WlrLayerShellHandler, WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
@@ -27,7 +26,7 @@ impl WlrLayerShellHandler for State {
|
||||
namespace: String,
|
||||
) {
|
||||
let output = if let Some(wl_output) = &wl_output {
|
||||
Output::from_resource(wl_output)
|
||||
self.niri.output_from_resource(wl_output)
|
||||
} else {
|
||||
self.niri.layout.active_output().cloned()
|
||||
};
|
||||
@@ -126,8 +125,10 @@ impl State {
|
||||
let output_size = output_size(&output);
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
|
||||
let hook = add_mapped_layer_pre_commit_hook(layer);
|
||||
let mapped = MappedLayer::new(
|
||||
layer.clone(),
|
||||
hook,
|
||||
rules,
|
||||
output_size,
|
||||
scale,
|
||||
@@ -142,6 +143,21 @@ impl State {
|
||||
if prev.is_some() {
|
||||
error!("MappedLayer was present for an unmapped surface");
|
||||
}
|
||||
} else {
|
||||
// The surface remains mapped.
|
||||
if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) {
|
||||
// Check if the layer changed.
|
||||
if mapped.take_recompute_rules_on_commit() {
|
||||
let config = self.niri.config.borrow();
|
||||
if mapped
|
||||
.recompute_layer_rules(&config.layer_rules, self.niri.is_at_startup)
|
||||
{
|
||||
mapped.update_config(&config);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("MappedLayer missing for a mapped surface");
|
||||
}
|
||||
}
|
||||
|
||||
// Give focus to newly mapped on-demand surfaces. Some launchers like lxqt-runner rely
|
||||
@@ -155,7 +171,7 @@ impl State {
|
||||
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand surfaces in
|
||||
// update_keyboard_focus(), so we don't need to check for that here.
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/641
|
||||
// https://github.com/niri-wm/niri/issues/641
|
||||
let on_demand = layer.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::OnDemand;
|
||||
if was_unmapped && on_demand {
|
||||
@@ -204,3 +220,23 @@ impl State {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn add_mapped_layer_pre_commit_hook(layer: &LayerSurface) -> HookId {
|
||||
add_pre_commit_hook::<State, _>(layer.wl_surface(), move |state, _dh, surface| {
|
||||
let layer_changed = with_states(surface, |states| {
|
||||
let mut guard = states.cached_state.get::<LayerSurfaceCachedState>();
|
||||
let pending_layer = guard.pending().layer;
|
||||
let current_layer = guard.current().layer;
|
||||
pending_layer != current_layer
|
||||
});
|
||||
|
||||
if layer_changed {
|
||||
for mapped in state.niri.mapped_layer_surfaces.values_mut() {
|
||||
if mapped.surface().wl_surface() == surface {
|
||||
mapped.set_recompute_rules_on_commit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+27
-6
@@ -1,3 +1,4 @@
|
||||
pub mod background_effect;
|
||||
mod compositor;
|
||||
mod layer_shell;
|
||||
mod xdg_shell;
|
||||
@@ -68,7 +69,7 @@ use smithay::{
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
delegate_viewporter, delegate_xdg_activation,
|
||||
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
@@ -279,6 +280,7 @@ impl KeyboardShortcutsInhibitHandler for State {
|
||||
|
||||
delegate_input_method_manager!(State);
|
||||
delegate_keyboard_shortcuts_inhibit!(State);
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
|
||||
impl SelectionHandler for State {
|
||||
type SelectionUserData = Arc<[u8]>;
|
||||
@@ -359,7 +361,7 @@ impl DndGrabHandler for State {
|
||||
trace!("dnd dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
|
||||
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
|
||||
// example. On successful drop, additionally activate the target window.
|
||||
@@ -381,10 +383,21 @@ impl DndGrabHandler for State {
|
||||
self.niri.layout.focus_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
|
||||
trace!("dnd cancelled");
|
||||
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::niri::Niri {
|
||||
fn on_maybe_dnd_ended(&mut self) {
|
||||
self.layout.dnd_end();
|
||||
self.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,7 +472,7 @@ impl SessionLockHandler for State {
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
let Some(output) = Output::from_resource(&output) else {
|
||||
let Some(output) = self.niri.output_from_resource(&output) else {
|
||||
warn!("no Output matching WlOutput");
|
||||
return;
|
||||
};
|
||||
@@ -544,7 +557,9 @@ impl ForeignToplevelHandler for State {
|
||||
{
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if let Some(requested_output) =
|
||||
wl_output.and_then(|o| self.niri.output_from_resource(&o))
|
||||
{
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
@@ -620,6 +635,12 @@ delegate_ext_workspace!(State);
|
||||
|
||||
impl ScreencopyHandler for State {
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// This can happen if the output was removed before this was called.
|
||||
if !self.niri.output_exists(screencopy.output()) {
|
||||
trace!("screencopy output no longer exists");
|
||||
return;
|
||||
}
|
||||
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
|
||||
@@ -612,7 +612,7 @@ impl XdgShellHandler for State {
|
||||
toplevel: ToplevelSurface,
|
||||
wl_output: Option<wl_output::WlOutput>,
|
||||
) {
|
||||
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
|
||||
let requested_output = wl_output.and_then(|o| self.niri.output_from_resource(&o));
|
||||
|
||||
if let Some((mapped, current_output)) = self
|
||||
.niri
|
||||
@@ -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();
|
||||
@@ -863,7 +861,15 @@ impl XdgShellHandler for State {
|
||||
|
||||
self.niri.window_mru_ui.remove_window(id);
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
|
||||
|
||||
let surface = surface.wl_surface();
|
||||
// This check is necessary because implicit resource destruction is done with
|
||||
// undefined order, so the surface might get destroyed before toplevel_destroyed() is
|
||||
// called. In this case, adding the default pre-commit hook here would leak it, since the
|
||||
// place that removes it is WlSurface::destroyed(), which had already been called by now.
|
||||
if surface.is_alive() {
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
}
|
||||
|
||||
// If this is the only instance, then this transaction will complete immediately, so no
|
||||
// need to set the timer.
|
||||
@@ -1437,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;
|
||||
};
|
||||
@@ -1539,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| {
|
||||
|
||||
@@ -4,7 +4,6 @@ use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::protocols::virtual_keyboard::VirtualKeyboard;
|
||||
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||
|
||||
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||
@@ -45,12 +44,6 @@ impl NiriInputDevice for WinitVirtualDevice {
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualKeyboard {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualPointer {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
self.output().cloned()
|
||||
|
||||
+33
-47
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use input::event::gesture::GestureEventCoordinates as _;
|
||||
use niri_config::{
|
||||
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger, Xkb,
|
||||
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger,
|
||||
};
|
||||
use niri_ipc::LayoutSwitchTarget;
|
||||
use smithay::backend::input::{
|
||||
@@ -41,6 +41,8 @@ use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
|
||||
use touch_overview_grab::TouchOverviewGrab;
|
||||
|
||||
use self::move_grab::MoveGrab;
|
||||
use self::pick_color_grab::PickColorGrab;
|
||||
use self::pick_window_grab::PickWindowGrab;
|
||||
use self::resize_grab::ResizeGrab;
|
||||
use self::spatial_movement_grab::SpatialMovementGrab;
|
||||
#[cfg(feature = "dbus")]
|
||||
@@ -48,7 +50,6 @@ use crate::dbus::freedesktop_a11y::KbMonBlock;
|
||||
use crate::layout::scrolling::ScrollDirection;
|
||||
use crate::layout::{ActivateWindow, LayoutElement as _};
|
||||
use crate::niri::{CastTarget, PointerVisibility, State};
|
||||
use crate::protocols::virtual_keyboard::VirtualKeyboard;
|
||||
use crate::ui::mru::{WindowMru, WindowMruUi};
|
||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||
use crate::utils::spawning::{spawn, spawn_sh};
|
||||
@@ -294,6 +295,7 @@ impl State {
|
||||
I::Device: 'static,
|
||||
{
|
||||
let device_output = event.device().output(self);
|
||||
let device_output = device_output.filter(|output| self.niri.output_exists(output));
|
||||
let device_output = device_output.as_ref();
|
||||
let (target_geo, keep_ratio, px, transform) =
|
||||
if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) {
|
||||
@@ -361,36 +363,11 @@ impl State {
|
||||
.is_some_and(KeyboardShortcutsInhibitor::is_active)
|
||||
}
|
||||
|
||||
fn on_keyboard<I: InputBackend + 'static>(
|
||||
fn on_keyboard<I: InputBackend>(
|
||||
&mut self,
|
||||
event: I::KeyboardKeyEvent,
|
||||
consumed_by_a11y: &mut bool,
|
||||
) where
|
||||
I::Device: 'static,
|
||||
{
|
||||
// Reset the keymap when handling a physical keyboard after a virtual one.
|
||||
if self.niri.reset_keymap {
|
||||
let device = event.device();
|
||||
let is_virtual_keyboard = (&device as &dyn Any)
|
||||
.downcast_ref::<VirtualKeyboard>()
|
||||
.is_some();
|
||||
if !is_virtual_keyboard {
|
||||
self.niri.reset_keymap = false;
|
||||
|
||||
let config = self.niri.config.borrow();
|
||||
let xkb_config = config.input.keyboard.xkb.clone();
|
||||
std::mem::drop(config);
|
||||
|
||||
if xkb_config != Xkb::default() {
|
||||
self.set_xkb_config(xkb_config.to_xkb_config());
|
||||
} else {
|
||||
// Use locale1 settings if xkb config is unset.
|
||||
let xkb = self.niri.xkb_from_locale1.clone().unwrap_or_default();
|
||||
self.set_xkb_config(xkb.to_xkb_config());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
) {
|
||||
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
|
||||
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
@@ -514,19 +491,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if pressed
|
||||
&& raw == Some(Keysym::Escape)
|
||||
&& (this.niri.pick_window.is_some() || this.niri.pick_color.is_some())
|
||||
{
|
||||
// We window picking state so the pick window grab must be active.
|
||||
// Unsetting it cancels window picking.
|
||||
this.niri
|
||||
.seat
|
||||
.get_pointer()
|
||||
.unwrap()
|
||||
.unset_grab(this, serial, time);
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(None);
|
||||
if pressed && raw == Some(Keysym::Escape) {
|
||||
// Cancel certain grabs on Escape.
|
||||
let pointer = this.niri.seat.get_pointer().unwrap();
|
||||
if pointer
|
||||
.with_grab(|_, grab| Self::grab_can_be_cancelled_with_esc(grab))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
pointer.unset_grab(this, serial, time);
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(None);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Keysym::space) = raw {
|
||||
@@ -2318,9 +2293,9 @@ impl State {
|
||||
}
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::LoadConfigFile => {
|
||||
Action::LoadConfigFile(path) => {
|
||||
if let Some(watcher) = &self.niri.config_file_watcher {
|
||||
watcher.load_config();
|
||||
watcher.load_config(path);
|
||||
}
|
||||
}
|
||||
Action::MruConfirm => {
|
||||
@@ -3286,8 +3261,8 @@ impl State {
|
||||
let horizontal_amount = event.amount(Axis::Horizontal);
|
||||
let vertical_amount = event.amount(Axis::Vertical);
|
||||
|
||||
// Handle touchpad scroll bindings.
|
||||
if source == AxisSource::Finger {
|
||||
// Handle touchpad and continuous scroll bindings.
|
||||
if source == AxisSource::Finger || source == AxisSource::Continuous {
|
||||
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
|
||||
let modifiers = modifiers_from_state(mods);
|
||||
|
||||
@@ -4060,6 +4035,7 @@ impl State {
|
||||
fallback_output: Option<&Output>,
|
||||
) -> Option<Point<f64, Logical>> {
|
||||
let output = evt.device().output(self);
|
||||
let output = output.filter(|output| self.niri.output_exists(output));
|
||||
let output = output.as_ref().or(fallback_output)?;
|
||||
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
|
||||
let transform = output.current_transform();
|
||||
@@ -4322,6 +4298,12 @@ impl State {
|
||||
// Null-source DnD: weston-dnd --self-only
|
||||
|| grab.is::<DnDGrab<Self, WlSurface, WlSurface>>()
|
||||
}
|
||||
|
||||
fn grab_can_be_cancelled_with_esc(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
|
||||
let grab = grab.as_any();
|
||||
|
||||
grab.is::<PickWindowGrab>() || grab.is::<PickColorGrab>() || Self::is_dnd_grab(grab)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the key should be intercepted and mark intercepted
|
||||
@@ -4704,7 +4686,11 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
|
||||
let _ = device.config_tap_set_enabled(c.tap);
|
||||
let _ = device.config_dwt_set_enabled(c.dwt);
|
||||
let _ = device.config_dwtp_set_enabled(c.dwtp);
|
||||
let _ = device.config_tap_set_drag_lock_enabled(c.drag_lock);
|
||||
let _ = device.config_tap_set_drag_lock_enabled(if c.drag_lock {
|
||||
input::DragLockState::EnabledTimeout
|
||||
} else {
|
||||
input::DragLockState::Disabled
|
||||
});
|
||||
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
|
||||
let _ = device.config_accel_set_speed(c.accel_speed.0);
|
||||
let _ = device.config_left_handed_set(c.left_handed);
|
||||
|
||||
@@ -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<State>,
|
||||
@@ -49,13 +49,13 @@ impl PickColorGrab {
|
||||
let pos = pos_within_output.to_physical_precise_floor(scale);
|
||||
let size = Size::<i32, Physical>::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,
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
|
||||
windows.sort_unstable_by_key(|a| a.id);
|
||||
|
||||
for window in windows {
|
||||
print_window(&window);
|
||||
|
||||
+9
-1
@@ -463,7 +463,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
fn validate_action(action: &Action) -> Result<(), String> {
|
||||
if let Action::Screenshot { path, .. }
|
||||
| Action::ScreenshotScreen { path, .. }
|
||||
| Action::ScreenshotWindow { path, .. } = action
|
||||
| Action::ScreenshotWindow { path, .. }
|
||||
| Action::LoadConfigFile { path } = action
|
||||
{
|
||||
if let Some(path) = path {
|
||||
// Relative paths are resolved against the niri compositor's working directory, which
|
||||
@@ -474,6 +475,13 @@ fn validate_action(action: &Action) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Action::LoadConfigFile { path: Some(path) } = action {
|
||||
let p = Path::new(path);
|
||||
if !p.is_file() {
|
||||
return Err(format!("path does not point to a file: {path}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+122
-25
@@ -2,19 +2,22 @@ use niri_config::utils::MergeWith as _;
|
||||
use niri_config::{Config, LayerRule};
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
use smithay::desktop::{LayerSurface, PopupKind, PopupManager};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
use smithay::wayland::compositor::{remove_pre_commit_hook, HookId};
|
||||
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::animation::Clock;
|
||||
use crate::layout::shadow::Shadow;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::background_effect::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::xray::XrayPos;
|
||||
use crate::render_helpers::{background_effect, RenderCtx};
|
||||
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -22,15 +25,26 @@ pub struct MappedLayer {
|
||||
/// The surface itself.
|
||||
surface: LayerSurface,
|
||||
|
||||
/// Pre-commit hook that we have on all mapped layer surfaces.
|
||||
pre_commit_hook: HookId,
|
||||
|
||||
/// Up-to-date rules.
|
||||
rules: ResolvedLayerRules,
|
||||
|
||||
/// Whether to recompute layer rules on the next commit.
|
||||
///
|
||||
/// Set in the pre-commit hook when the layer changes; consumed in the commit handler.
|
||||
recompute_rules_on_commit: bool,
|
||||
|
||||
/// Buffer to draw instead of the surface when it should be blocked out.
|
||||
block_out_buffer: SolidColorBuffer,
|
||||
|
||||
/// The shadow around the surface.
|
||||
shadow: Shadow,
|
||||
|
||||
/// The blur config, passed for background effect rendering.
|
||||
blur_config: niri_config::Blur,
|
||||
|
||||
/// The view size for the layer surface's output.
|
||||
view_size: Size<f64, Logical>,
|
||||
|
||||
@@ -46,12 +60,14 @@ niri_render_elements! {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Shadow = ShadowRenderElement,
|
||||
BackgroundEffect = BackgroundEffectElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(
|
||||
surface: LayerSurface,
|
||||
pre_commit_hook: HookId,
|
||||
rules: ResolvedLayerRules,
|
||||
view_size: Size<f64, Logical>,
|
||||
scale: f64,
|
||||
@@ -65,11 +81,14 @@ impl MappedLayer {
|
||||
|
||||
Self {
|
||||
surface,
|
||||
pre_commit_hook,
|
||||
rules,
|
||||
recompute_rules_on_commit: false,
|
||||
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||
view_size,
|
||||
scale,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
blur_config: config.blur,
|
||||
clock,
|
||||
}
|
||||
}
|
||||
@@ -80,6 +99,8 @@ impl MappedLayer {
|
||||
shadow_config.on = false;
|
||||
shadow_config.merge_with(&self.rules.shadow);
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
self.blur_config = config.blur;
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
@@ -128,6 +149,14 @@ impl MappedLayer {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_recompute_rules_on_commit(&mut self) {
|
||||
self.recompute_rules_on_commit = true;
|
||||
}
|
||||
|
||||
pub fn take_recompute_rules_on_commit(&mut self) -> bool {
|
||||
std::mem::take(&mut self.recompute_rules_on_commit)
|
||||
}
|
||||
|
||||
pub fn place_within_backdrop(&self) -> bool {
|
||||
if !self.rules.place_within_backdrop {
|
||||
return false;
|
||||
@@ -157,16 +186,23 @@ impl MappedLayer {
|
||||
|
||||
pub fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
ns: Option<usize>,
|
||||
location: Point<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
|
||||
) {
|
||||
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) {
|
||||
let bob_offset = self.bob_offset();
|
||||
let location = location + bob_offset;
|
||||
let xray_pos = xray_pos.offset(bob_offset);
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
|
||||
let should_block_out = ctx.target.should_block_out(self.rules.block_out_from);
|
||||
if should_block_out {
|
||||
// Round to physical pixels.
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
@@ -182,9 +218,8 @@ impl MappedLayer {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
push_elements_from_surface_tree(
|
||||
renderer,
|
||||
ctx.renderer,
|
||||
surface,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
@@ -196,41 +231,103 @@ 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()));
|
||||
|
||||
let geometry = Rectangle::new(location, self.block_out_buffer.size());
|
||||
let surface_off = Point::new(0., 0.); // No geometry on layer surfaces.
|
||||
let surface_anim_scale = Scale::from(1.);
|
||||
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
|
||||
background_effect::render_for_tile(
|
||||
ctx.as_gles(),
|
||||
ns,
|
||||
geometry,
|
||||
self.scale,
|
||||
false,
|
||||
surface,
|
||||
surface_off,
|
||||
surface_anim_scale,
|
||||
self.blur_config,
|
||||
radius,
|
||||
self.rules.background_effect,
|
||||
should_block_out,
|
||||
xray_pos,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render_popups<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
ns: Option<usize>,
|
||||
location: Point<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
let scale = Scale::from(self.scale);
|
||||
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
|
||||
let bob_offset = self.bob_offset();
|
||||
let location = location + bob_offset;
|
||||
let xray_pos = xray_pos.offset(bob_offset);
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let offset = popup_offset - popup.geometry().loc;
|
||||
for (popup, offset) in PopupManager::popups_for_surface(surface) {
|
||||
let popup_rules = match popup {
|
||||
PopupKind::Xdg(_) => self.rules.popups,
|
||||
// IME popups aren't affected by rules for regular popups.
|
||||
PopupKind::InputMethod(_) => niri_config::ResolvedPopupsRules::default(),
|
||||
};
|
||||
let alpha = alpha * popup_rules.opacity.unwrap_or(1.).clamp(0., 1.);
|
||||
|
||||
let surface = popup.wl_surface();
|
||||
let popup_geo = popup.geometry();
|
||||
let surface_loc = location + (offset - popup_geo.loc).to_f64();
|
||||
|
||||
push_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
ctx.renderer,
|
||||
surface,
|
||||
surface_loc.to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::ScanoutCandidate,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
|
||||
let geometry = Rectangle::new(location + offset.to_f64(), popup_geo.size.to_f64());
|
||||
let surface_off = popup_geo.loc.upscale(-1).to_f64();
|
||||
let surface_anim_scale = Scale::from(1.);
|
||||
let mut effect = popup_rules.background_effect;
|
||||
// Default xray to false for pop-ups since they're always on top of something.
|
||||
if effect.xray.is_none() {
|
||||
effect.xray = Some(false);
|
||||
}
|
||||
let xray_pos = xray_pos.offset(offset.to_f64());
|
||||
background_effect::render_for_tile(
|
||||
ctx.as_gles(),
|
||||
ns,
|
||||
geometry,
|
||||
self.scale,
|
||||
false,
|
||||
surface,
|
||||
surface_off,
|
||||
surface_anim_scale,
|
||||
self.blur_config,
|
||||
popup_rules.geometry_corner_radius.unwrap_or_default(),
|
||||
effect,
|
||||
false,
|
||||
xray_pos,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MappedLayer {
|
||||
fn drop(&mut self) {
|
||||
remove_pre_commit_hook(self.surface.wl_surface(), &self.pre_commit_hook);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-1
@@ -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, ResolvedPopupsRules, ShadowRule};
|
||||
use smithay::desktop::LayerSurface;
|
||||
use smithay::wayland::shell::wlr_layer::Layer;
|
||||
|
||||
pub mod mapped;
|
||||
pub use mapped::MappedLayer;
|
||||
@@ -26,6 +27,12 @@ pub struct ResolvedLayerRules {
|
||||
|
||||
/// Whether to bob this window up and down.
|
||||
pub baba_is_float: bool,
|
||||
|
||||
/// Background effect configuration.
|
||||
pub background_effect: BackgroundEffect,
|
||||
|
||||
/// Rules for this layer surface's popups.
|
||||
pub popups: ResolvedPopupsRules,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
@@ -70,6 +77,12 @@ impl ResolvedLayerRules {
|
||||
}
|
||||
|
||||
resolved.shadow.merge_with(&rule.shadow);
|
||||
|
||||
resolved
|
||||
.background_effect
|
||||
.merge_with(&rule.background_effect);
|
||||
|
||||
resolved.popups.merge_with(&rule.popups);
|
||||
}
|
||||
|
||||
resolved
|
||||
@@ -83,5 +96,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
|
||||
}
|
||||
|
||||
@@ -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<GlesTexture>,
|
||||
|
||||
/// 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<TextureBuffer<GlesTexture>>,
|
||||
|
||||
/// Blocked-out contents of the window.
|
||||
blocked_out_buffer: TextureBuffer<GlesTexture>,
|
||||
|
||||
@@ -44,6 +50,9 @@ pub struct ClosingWindow {
|
||||
/// How much the texture should be offset.
|
||||
buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// How much the texture with blocked-out bg should be offset.
|
||||
buffer_with_blocked_out_bg_offset: Point<f64, Logical>,
|
||||
|
||||
/// How much the blocked-out texture should be offset.
|
||||
blocked_out_buffer_offset: Point<f64, Logical>,
|
||||
|
||||
@@ -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<GlesRenderer>,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
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);
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ 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::xray::XrayPos;
|
||||
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,
|
||||
@@ -489,6 +490,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
// Now, descendants is in back-to-front order, and repositioning them in the front-to-back
|
||||
// order will preserve the subsequent indices and work out right.
|
||||
let mut idx = idx;
|
||||
#[allow(clippy::explicit_counter_loop)]
|
||||
for descendant_idx in descendants.into_iter().rev() {
|
||||
self.raise_window(descendant_idx, idx);
|
||||
idx += 1;
|
||||
@@ -1055,9 +1057,9 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(FloatingSpaceRenderElement<R>),
|
||||
) {
|
||||
@@ -1067,7 +1069,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
//
|
||||
// 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,7 +1078,8 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
// 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| {
|
||||
let xray_pos = xray_pos.offset(tile_pos);
|
||||
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
+117
-29
@@ -59,12 +59,14 @@ use crate::animation::{Animation, Clock};
|
||||
use crate::input::swipe_tracker::SwipeTracker;
|
||||
use crate::layout::scrolling::ScrollDirection;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::background_effect::BackgroundEffectElement;
|
||||
use crate::render_helpers::offscreen::OffscreenData;
|
||||
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, XrayPos};
|
||||
use crate::render_helpers::{BakedBuffer, RenderCtx};
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
use crate::utils::{
|
||||
@@ -112,6 +114,7 @@ niri_render_elements! {
|
||||
LayoutElementRenderElement<R> => {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
BackgroundEffect = BackgroundEffectElement,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +135,11 @@ pub trait LayoutElement {
|
||||
/// Unique ID of this element.
|
||||
fn id(&self) -> &Self::Id;
|
||||
|
||||
/// Updates the config for the element.
|
||||
fn update_config(&mut self, blur_config: niri_config::Blur) {
|
||||
let _ = blur_config;
|
||||
}
|
||||
|
||||
/// Visual size of the element.
|
||||
///
|
||||
/// This is what the user would consider the size, i.e. excluding CSD shadows and whatnot.
|
||||
@@ -154,41 +162,55 @@ pub trait LayoutElement {
|
||||
/// location.
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
target: RenderTarget,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
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, xray_pos, push);
|
||||
self.render_normal(ctx.r(), location, scale, alpha, push);
|
||||
}
|
||||
|
||||
/// Renders the non-popup parts of the element.
|
||||
fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
target: RenderTarget,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let _ = (renderer, location, scale, alpha, target, push);
|
||||
let _ = (ctx, location, scale, alpha, push);
|
||||
}
|
||||
|
||||
/// Renders the popups of the element.
|
||||
fn render_popups<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
target: RenderTarget,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let _ = (renderer, location, scale, alpha, target, push);
|
||||
let _ = (ctx, location, scale, alpha, xray_pos, push);
|
||||
}
|
||||
|
||||
/// Renders the background effect behind the main surface of the element.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_background_effect(
|
||||
&self,
|
||||
_ctx: RenderCtx<GlesRenderer>,
|
||||
_geometry: Rectangle<f64, Logical>,
|
||||
_scale: f64,
|
||||
_clip_to_geometry: bool,
|
||||
_surface_anim_scale: Scale<f64>,
|
||||
_radius: CornerRadius,
|
||||
_xray_pos: XrayPos,
|
||||
_push: &mut dyn FnMut(BackgroundEffectElement),
|
||||
) {
|
||||
}
|
||||
|
||||
/// Requests the element to change its size.
|
||||
@@ -268,6 +290,9 @@ pub trait LayoutElement {
|
||||
Some(requested)
|
||||
}
|
||||
|
||||
fn is_windowed_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_pending_windowed_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -275,6 +300,22 @@ pub trait LayoutElement {
|
||||
let _ = value;
|
||||
}
|
||||
|
||||
/// The effective geometry corner radius for this element.
|
||||
///
|
||||
/// Returns zero when the element is in windowed fullscreen, since fullscreen windows have
|
||||
/// square corners.
|
||||
///
|
||||
/// This method only handles windowed fullscreen and not maximized/real fullscreen. This is
|
||||
/// because windowed fullscreen is handled by the element itself, whereas other sizing modes
|
||||
/// are handled externally by the Tile, so the corner radius changes for those modes is also
|
||||
/// handled externally.
|
||||
fn geometry_corner_radius(&self) -> CornerRadius {
|
||||
if self.is_windowed_fullscreen() {
|
||||
return CornerRadius::default();
|
||||
}
|
||||
self.rules().geometry_corner_radius.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_child_of(&self, parent: &Self) -> bool;
|
||||
|
||||
fn rules(&self) -> &ResolvedWindowRules;
|
||||
@@ -351,6 +392,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,
|
||||
@@ -611,6 +653,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,
|
||||
@@ -2732,8 +2775,18 @@ impl<W: LayoutElement> Layout<W> {
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
|
||||
if output.is_none_or(|output| move_.output == *output) {
|
||||
let pos_within_output = move_.tile_render_location(zoom);
|
||||
|
||||
// We're not on any specific workspace so we can't compute a "workspace view" rect.
|
||||
// Let's instead compute a rect relative to the output.
|
||||
//
|
||||
// FIXME: we could make the colors match up better in the overview by figuring out
|
||||
// where a centered workspace would currently be, and computing the view rect
|
||||
// against that. Since most of the time the dragged window will be on a centered
|
||||
// workspace.
|
||||
let view_rect =
|
||||
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
|
||||
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
|
||||
.downscale(zoom);
|
||||
|
||||
move_.tile.update_render_elements(true, view_rect);
|
||||
}
|
||||
}
|
||||
@@ -2746,7 +2799,9 @@ impl<W: LayoutElement> Layout<W> {
|
||||
..
|
||||
} = &mut self.monitor_set
|
||||
else {
|
||||
error!("update_render_elements called with no monitors");
|
||||
if output.is_some() {
|
||||
error!("update_render_elements called with no monitors but Some output");
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -2818,13 +2873,12 @@ impl<W: LayoutElement> Layout<W> {
|
||||
ws.scrolling_insert_position(pos_within_workspace)
|
||||
};
|
||||
|
||||
let rules = move_.tile.window().rules();
|
||||
let border_width = move_.tile.effective_border_width().unwrap_or(0.);
|
||||
let corner_radius = rules
|
||||
.geometry_corner_radius
|
||||
.map_or(CornerRadius::default(), |radius| {
|
||||
radius.expanded_by(border_width as f32)
|
||||
});
|
||||
let corner_radius = move_
|
||||
.tile
|
||||
.window()
|
||||
.geometry_corner_radius()
|
||||
.expanded_by(border_width as f32);
|
||||
mon.insert_hint = Some(InsertHint {
|
||||
workspace: insert_ws,
|
||||
position,
|
||||
@@ -4596,12 +4650,33 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
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_within_output = move_.tile_render_location(zoom);
|
||||
|
||||
// Computation matches update_render_elements().
|
||||
let view_rect =
|
||||
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
|
||||
.downscale(zoom);
|
||||
move_.tile.update_render_elements(false, view_rect);
|
||||
|
||||
move_.tile.store_unmap_snapshot_if_empty(
|
||||
renderer,
|
||||
xray,
|
||||
xray_has_blocked_out_layers,
|
||||
XrayPos::new(pos_within_output, zoom),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4609,9 +4684,15 @@ impl<W: LayoutElement> Layout<W> {
|
||||
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,
|
||||
XrayPos::new(geo.loc, zoom),
|
||||
window,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4620,7 +4701,13 @@ impl<W: LayoutElement> Layout<W> {
|
||||
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,
|
||||
XrayPos::default(),
|
||||
window,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -4721,9 +4808,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
pub fn render_interactive_move_for_output<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
ctx: RenderCtx<R>,
|
||||
output: &Output,
|
||||
target: RenderTarget,
|
||||
push: &mut dyn FnMut(RescaleRenderElement<TileRenderElement<R>>),
|
||||
) {
|
||||
if self.update_render_elements_time != self.clock.now() {
|
||||
@@ -4740,13 +4826,15 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
let scale = Scale::from(move_.output.current_scale().fractional_scale());
|
||||
let zoom = self.overview_zoom();
|
||||
let location = move_.tile_render_location(zoom);
|
||||
let pos_in_backdrop = move_.tile_render_location(zoom);
|
||||
let xray_pos = XrayPos::new(pos_in_backdrop, zoom);
|
||||
|
||||
move_
|
||||
.tile
|
||||
.render(renderer, location, true, target, &mut |elem| {
|
||||
.render(ctx, pos_in_backdrop, xray_pos, true, &mut |elem| {
|
||||
push(RescaleRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
pos_in_backdrop.to_physical_precise_round(scale),
|
||||
zoom,
|
||||
));
|
||||
});
|
||||
|
||||
@@ -24,7 +24,8 @@ 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::xray::XrayPos;
|
||||
use crate::render_helpers::RenderCtx;
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{
|
||||
@@ -1669,8 +1670,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
pub fn render_workspaces<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
mut ctx: RenderCtx<R>,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
@@ -1734,16 +1734,18 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}};
|
||||
}
|
||||
|
||||
ws.render_floating(renderer, target, focus_ring, push!());
|
||||
let xray_pos = XrayPos::new(geo.loc, zoom);
|
||||
|
||||
ws.render_floating(ctx.r(), xray_pos, 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(), xray_pos, focus_ring, push!());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ 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::xray::XrayPos;
|
||||
use crate::render_helpers::RenderCtx;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
use crate::utils::ResizeEdge;
|
||||
use crate::window::ResolvedWindowRules;
|
||||
@@ -2899,8 +2900,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
mut ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(ScrollingSpaceRenderElement<R>),
|
||||
) {
|
||||
@@ -2909,7 +2910,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
// 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<W: LayoutElement> ScrollingSpace<W> {
|
||||
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,7 +2956,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
continue;
|
||||
}
|
||||
|
||||
tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
|
||||
let xray_pos = xray_pos.offset(tile_pos);
|
||||
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}));
|
||||
}
|
||||
@@ -241,6 +243,10 @@ impl LayoutElement for TestWindow {
|
||||
self.0.requested_size.get()
|
||||
}
|
||||
|
||||
fn is_windowed_fullscreen(&self) -> bool {
|
||||
self.0.is_windowed_fullscreen.get()
|
||||
}
|
||||
|
||||
fn is_pending_windowed_fullscreen(&self) -> bool {
|
||||
self.0.is_pending_windowed_fullscreen.get()
|
||||
}
|
||||
|
||||
+195
-77
@@ -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::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, XrayPos};
|
||||
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,
|
||||
@@ -130,6 +132,7 @@ niri_render_elements! {
|
||||
ClippedSurface = ClippedSurfaceRenderElement<R>,
|
||||
Offscreen = OffscreenRenderElement,
|
||||
ExtraDamage = ExtraDamage,
|
||||
BackgroundEffect = BackgroundEffectElement,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +251,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
|
||||
let shadow_config = self.options.layout.shadow.merged_with(&rules.shadow);
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
self.window.update_config(self.options.blur);
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
@@ -398,12 +403,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
let window_size = self.window_size();
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.unwrap_or_default()
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.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) {
|
||||
@@ -469,11 +473,22 @@ impl<W: LayoutElement> Tile<W> {
|
||||
border_window_size.w -= border_width * 2.;
|
||||
border_window_size.h -= border_width * 2.;
|
||||
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.map_or(CornerRadius::default(), |radius| {
|
||||
radius.expanded_by(border_width as f32)
|
||||
})
|
||||
// FIXME: this takes into account the animation from normal sizing mode to
|
||||
// maximized/fullscreen, but it doesn't take into account the corner radius animation from
|
||||
// the window itself.
|
||||
//
|
||||
// Currently, an easy way to see the problem is to start from a window with a nonzero
|
||||
// radius, then go from windowed fullscreen (that forces 0 radius) to regular fullscreen.
|
||||
// At the start of the animation, windowed fullscreen becomes false, but the window hasn't
|
||||
// animated to the normal fullscreen yet, so the radius here jumps to its nonzero value,
|
||||
// even though it should remain zero throughout.
|
||||
//
|
||||
// Later, when windows get the surface shape protocol with radii, this issue will happen
|
||||
// when that changes between animated commits.
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.expanded_by(border_width as f32)
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
self.border.update_render_elements(
|
||||
border_window_size,
|
||||
@@ -492,9 +507,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let radius = if self.visual_border_width().is_some() {
|
||||
radius
|
||||
} else {
|
||||
rules
|
||||
.geometry_corner_radius
|
||||
.unwrap_or_default()
|
||||
self.window
|
||||
.geometry_corner_radius()
|
||||
.scaled_by(1. - expanded_progress as f32)
|
||||
};
|
||||
self.shadow.update_render_elements(
|
||||
@@ -1009,10 +1023,10 @@ impl<W: LayoutElement> Tile<W> {
|
||||
|
||||
fn render_inner<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
mut xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
push: &mut dyn FnMut(TileRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Tile::render_inner");
|
||||
@@ -1039,67 +1053,69 @@ impl<W: LayoutElement> Tile<W> {
|
||||
//
|
||||
// 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();
|
||||
let bob_offset = self.bob_offset();
|
||||
let location = location + bob_offset;
|
||||
xray_pos = xray_pos.offset(bob_offset);
|
||||
|
||||
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;
|
||||
let area = Rectangle::new(window_render_loc, animated_window_size);
|
||||
xray_pos = xray_pos.offset(window_loc);
|
||||
|
||||
let rules = self.window.rules();
|
||||
|
||||
// Clip to geometry including during the fullscreen animation to help with buggy clients
|
||||
// that submit a full-sized buffer before acking the fullscreen state (Firefox).
|
||||
let clip_to_geometry = fullscreen_progress < 1. && rules.clip_to_geometry == Some(true);
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.unwrap_or_default()
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
|
||||
// Popups go on top, whether it's resize or not.
|
||||
self.window.render_popups(
|
||||
renderer,
|
||||
ctx.r(),
|
||||
window_render_loc,
|
||||
scale,
|
||||
win_alpha,
|
||||
target,
|
||||
xray_pos,
|
||||
&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 +1164,12 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -1203,21 +1219,23 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// Otherwise, render the solid color as is.
|
||||
LayoutElementRenderElement::SolidColor(elem).into()
|
||||
}
|
||||
elem @ LayoutElementRenderElement::BackgroundEffect(_) => {
|
||||
// This is only used on popups for now. If subsurface blur is implemented, this
|
||||
// will need to be handled somehow.
|
||||
error!("background effect clipping is unimplemented");
|
||||
elem.into()
|
||||
}
|
||||
};
|
||||
|
||||
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. {
|
||||
@@ -1227,11 +1245,10 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// animated corner radius.
|
||||
if fullscreen_progress < 1. && has_border_shader {
|
||||
let border_width = self.visual_border_width().unwrap_or(0.);
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
.map_or(CornerRadius::default(), |radius| {
|
||||
radius.expanded_by(border_width as f32)
|
||||
})
|
||||
let radius = self
|
||||
.window
|
||||
.geometry_corner_radius()
|
||||
.expanded_by(border_width as f32)
|
||||
.scaled_by(1. - expanded_progress as f32);
|
||||
|
||||
let size = self.fullscreen_backdrop.size();
|
||||
@@ -1264,7 +1281,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
|
||||
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 +1293,33 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// 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()));
|
||||
}
|
||||
|
||||
let surface_anim_scale = animated_window_size / window_size;
|
||||
self.window.render_background_effect(
|
||||
ctx.as_gles(),
|
||||
area,
|
||||
self.scale,
|
||||
clip_to_geometry,
|
||||
surface_anim_scale,
|
||||
radius,
|
||||
xray_pos,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
push: &mut dyn FnMut(TileRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Tile::render");
|
||||
@@ -1306,17 +1335,17 @@ impl<W: LayoutElement> Tile<W> {
|
||||
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.),
|
||||
xray_pos,
|
||||
focus_ring,
|
||||
target,
|
||||
&mut |elem| elements.push(elem),
|
||||
);
|
||||
match open.render(
|
||||
renderer,
|
||||
ctx.renderer,
|
||||
&elements,
|
||||
self.animated_tile_size(),
|
||||
location,
|
||||
@@ -1333,16 +1362,16 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
} 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.),
|
||||
xray_pos,
|
||||
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 +1387,137 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
if !pushed {
|
||||
self.render_inner(renderer, location, focus_ring, target, &mut |elem| {
|
||||
push(elem)
|
||||
});
|
||||
self.render_inner(ctx, location, xray_pos, 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,
|
||||
xray_pos: XrayPos,
|
||||
) {
|
||||
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, xray_pos));
|
||||
}
|
||||
|
||||
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,
|
||||
xray_pos: XrayPos,
|
||||
) -> 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.)),
|
||||
xray_pos,
|
||||
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.)),
|
||||
xray_pos,
|
||||
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.)),
|
||||
xray_pos,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
+26
-15
@@ -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, XrayPos};
|
||||
use crate::render_helpers::RenderCtx;
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
use crate::utils::{
|
||||
@@ -1626,22 +1627,22 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
pub fn render_scrolling<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
|
||||
) {
|
||||
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
|
||||
self.scrolling
|
||||
.render(renderer, target, scrolling_focus_ring, &mut |elem| {
|
||||
.render(ctx, xray_pos, scrolling_focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_floating<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
|
||||
) {
|
||||
@@ -1651,13 +1652,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
let floating_focus_ring = focus_ring && self.floating_is_active();
|
||||
self.floating.render(
|
||||
renderer,
|
||||
view_rect,
|
||||
target,
|
||||
floating_focus_ring,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
self.floating
|
||||
.render(ctx, xray_pos, view_rect, floating_focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_shadow<R: NiriRenderer>(
|
||||
@@ -1689,14 +1687,27 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
) || !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,
|
||||
xray_pos: XrayPos,
|
||||
window: &W::Id,
|
||||
) {
|
||||
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 xray_pos = xray_pos.offset(tile_pos);
|
||||
tile.store_unmap_snapshot_if_empty(
|
||||
renderer,
|
||||
xray,
|
||||
xray_has_blocked_out_layers,
|
||||
xray_pos,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-1
@@ -55,6 +55,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.compact()
|
||||
.with_writer(io::stderr)
|
||||
.with_env_filter(env_filter)
|
||||
.with_ansi_sanitization(false)
|
||||
.init();
|
||||
|
||||
if env::var_os("NOTIFY_SOCKET").is_some() {
|
||||
@@ -169,6 +170,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Create the compositor.
|
||||
let display = Display::new().unwrap();
|
||||
|
||||
// Increase the buffer size so that it's harder to crash a frozen client with a 1000 Hz mouse.
|
||||
set_default_max_buffer_size(&display, 1024 * 1024);
|
||||
|
||||
let mut state = State::new(
|
||||
config,
|
||||
event_loop.handle(),
|
||||
@@ -230,7 +235,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").is_none_or(|x| x != "1") {
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
if let Err(err) = sd_notify::notify(&[NotifyState::Ready]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
};
|
||||
|
||||
@@ -369,3 +374,37 @@ fn notify_fd() -> anyhow::Result<()> {
|
||||
notif.write_all(b"READY=1\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The wayland-server crate has set_default_max_buffer_size() under a libwayland_1_23 feature, but
|
||||
// this hard-requires libwayland-server >= 1.23 which is not present on e.g. Ubuntu 24.04. Since
|
||||
// calling this is an optional enhancement, do it optionally at runtime.
|
||||
fn set_default_max_buffer_size(display: &Display<State>, size: usize) {
|
||||
use std::ffi::c_void;
|
||||
|
||||
unsafe {
|
||||
// RTLD_NOLOAD ensures we only get a handle to the libwayland-server that wayland-rs has
|
||||
// already loaded into this process, rather than potentially pulling in a different copy.
|
||||
let lib = libc::dlopen(
|
||||
c"libwayland-server.so.0".as_ptr(),
|
||||
libc::RTLD_LAZY | libc::RTLD_NOLOAD,
|
||||
);
|
||||
if lib.is_null() {
|
||||
// It's not really expected that this can happen, maybe if some distro changes the
|
||||
// library name?
|
||||
warn!("cannot set default max buffer size: libwayland-server.so.0 is not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
let sym = libc::dlsym(lib, c"wl_display_set_default_max_buffer_size".as_ptr());
|
||||
if sym.is_null() {
|
||||
// Expected on libwayland-server < 1.23.
|
||||
trace!("wl_display_set_default_max_buffer_size is missing; skipping");
|
||||
} else {
|
||||
let func: unsafe extern "C" fn(*mut c_void, libc::size_t) = std::mem::transmute(sym);
|
||||
let display_ptr = display.handle().backend_handle().display_ptr();
|
||||
func(display_ptr.cast(), size);
|
||||
}
|
||||
|
||||
libc::dlclose(lib);
|
||||
}
|
||||
}
|
||||
|
||||
+525
-166
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,16 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::{
|
||||
ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
|
||||
};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr;
|
||||
use smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
|
||||
};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -12,22 +18,20 @@ use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::{
|
||||
ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes,
|
||||
ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes
|
||||
};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::window::mapped::MappedId;
|
||||
use crate::utils::with_toplevel_role_and_current;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
const EXT_LIST_VERSION: u32 = 1;
|
||||
const WLR_MANAGEMENT_VERSION: u32 = 3;
|
||||
|
||||
pub struct ForeignToplevelManagerState {
|
||||
display: DisplayHandle,
|
||||
instances: Vec<ZwlrForeignToplevelManagerV1>,
|
||||
ext_list_instances: HashSet<ExtForeignToplevelListV1>,
|
||||
wlr_management_instances: HashSet<ZwlrForeignToplevelManagerV1>,
|
||||
toplevels: HashMap<WlSurface, ToplevelData>,
|
||||
}
|
||||
|
||||
@@ -42,33 +46,45 @@ pub trait ForeignToplevelHandler {
|
||||
}
|
||||
|
||||
struct ToplevelData {
|
||||
identifier: MappedId,
|
||||
title: Option<String>,
|
||||
app_id: Option<String>,
|
||||
states: ArrayVec<u32, 3>,
|
||||
output: Option<Output>,
|
||||
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||
|
||||
ext_list_instances: HashSet<ExtForeignToplevelHandleV1>,
|
||||
wlr_management_instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||
// FIXME: parent.
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ForeignToplevelGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
filter: Arc<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
impl ForeignToplevelManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||
D: GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
D: Dispatch<ExtForeignToplevelListV1, ()>,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = ForeignToplevelGlobalData {
|
||||
filter: Box::new(filter),
|
||||
filter: Arc::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
|
||||
display
|
||||
.create_global::<D, ExtForeignToplevelListV1, _>(EXT_LIST_VERSION, global_data.clone());
|
||||
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(
|
||||
WLR_MANAGEMENT_VERSION,
|
||||
global_data,
|
||||
);
|
||||
Self {
|
||||
display: display.clone(),
|
||||
instances: Vec::new(),
|
||||
ext_list_instances: HashSet::new(),
|
||||
wlr_management_instances: HashSet::new(),
|
||||
toplevels: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -85,7 +101,11 @@ pub fn refresh(state: &mut State) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for instance in data.instances.keys() {
|
||||
for instance in data.ext_list_instances.iter() {
|
||||
instance.closed();
|
||||
}
|
||||
|
||||
for instance in data.wlr_management_instances.keys() {
|
||||
instance.closed();
|
||||
}
|
||||
|
||||
@@ -107,15 +127,23 @@ pub fn refresh(state: &mut State) {
|
||||
};
|
||||
|
||||
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
|
||||
focused = Some((mapped.window.clone(), output.cloned()));
|
||||
focused = Some((mapped.id(), mapped.window.clone(), output.cloned()));
|
||||
} else {
|
||||
refresh_toplevel(protocol_state, wl_surface, role, cur, output, false);
|
||||
refresh_toplevel(
|
||||
protocol_state,
|
||||
wl_surface,
|
||||
mapped.id(),
|
||||
role,
|
||||
cur,
|
||||
output,
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Finally, refresh the focused window.
|
||||
if let Some((window, output)) = focused {
|
||||
if let Some((identifier, window, output)) = focused {
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role_and_current(toplevel, |role, cur| {
|
||||
@@ -124,7 +152,15 @@ pub fn refresh(state: &mut State) {
|
||||
return;
|
||||
};
|
||||
|
||||
refresh_toplevel(protocol_state, wl_surface, role, cur, output.as_ref(), true);
|
||||
refresh_toplevel(
|
||||
protocol_state,
|
||||
wl_surface,
|
||||
identifier,
|
||||
role,
|
||||
cur,
|
||||
output.as_ref(),
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -142,7 +178,7 @@ pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput)
|
||||
continue;
|
||||
}
|
||||
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
for (instance, outputs) in &mut data.wlr_management_instances {
|
||||
if instance.client().as_ref() != Some(&client) {
|
||||
continue;
|
||||
}
|
||||
@@ -157,6 +193,7 @@ pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput)
|
||||
fn refresh_toplevel(
|
||||
protocol_state: &mut ForeignToplevelManagerState,
|
||||
wl_surface: &WlSurface,
|
||||
identifier: MappedId,
|
||||
role: &XdgToplevelSurfaceRoleAttributes,
|
||||
current: &ToplevelState,
|
||||
output: Option<&Output>,
|
||||
@@ -201,11 +238,24 @@ fn refresh_toplevel(
|
||||
output_changed = true;
|
||||
}
|
||||
|
||||
let something_changed =
|
||||
let something_changed_for_ext = new_title.is_some() || new_app_id.is_some();
|
||||
let something_changed_for_wlr =
|
||||
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
|
||||
|
||||
if something_changed {
|
||||
for (instance, outputs) in &mut data.instances {
|
||||
if something_changed_for_ext {
|
||||
for instance in &data.ext_list_instances {
|
||||
if let Some(new_title) = new_title {
|
||||
instance.title(new_title.to_owned());
|
||||
}
|
||||
if let Some(new_app_id) = new_app_id {
|
||||
instance.app_id(new_app_id.to_owned());
|
||||
}
|
||||
instance.done();
|
||||
}
|
||||
}
|
||||
|
||||
if something_changed_for_wlr {
|
||||
for (instance, outputs) in &mut data.wlr_management_instances {
|
||||
if let Some(new_title) = new_title {
|
||||
instance.title(new_title.to_owned());
|
||||
}
|
||||
@@ -232,7 +282,7 @@ fn refresh_toplevel(
|
||||
}
|
||||
}
|
||||
|
||||
for outputs in data.instances.values_mut() {
|
||||
for outputs in data.wlr_management_instances.values_mut() {
|
||||
// Clean up dead wl_outputs.
|
||||
outputs.retain(|x| x.is_alive());
|
||||
}
|
||||
@@ -240,16 +290,24 @@ fn refresh_toplevel(
|
||||
Entry::Vacant(entry) => {
|
||||
// New window, start tracking it.
|
||||
let mut data = ToplevelData {
|
||||
identifier,
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
states,
|
||||
output: output.cloned(),
|
||||
instances: HashMap::new(),
|
||||
ext_list_instances: HashSet::new(),
|
||||
wlr_management_instances: HashMap::new(),
|
||||
};
|
||||
|
||||
for manager in &protocol_state.instances {
|
||||
for manager in &protocol_state.ext_list_instances {
|
||||
if let Some(client) = manager.client() {
|
||||
data.add_instance::<State>(&protocol_state.display, &client, manager);
|
||||
data.add_ext_instance::<State>(&protocol_state.display, &client, manager);
|
||||
}
|
||||
}
|
||||
|
||||
for manager in &protocol_state.wlr_management_instances {
|
||||
if let Some(client) = manager.client() {
|
||||
data.add_wlr_instance::<State>(&protocol_state.display, &client, manager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +317,35 @@ fn refresh_toplevel(
|
||||
}
|
||||
|
||||
impl ToplevelData {
|
||||
fn add_instance<D>(
|
||||
fn add_ext_instance<D>(
|
||||
&mut self,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: &ExtForeignToplevelListV1,
|
||||
) where
|
||||
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let toplevel = client
|
||||
.create_resource::<ExtForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
|
||||
.unwrap();
|
||||
manager.toplevel(&toplevel);
|
||||
|
||||
toplevel.identifier(self.identifier.to_protocol_identifier());
|
||||
|
||||
if let Some(title) = &self.title {
|
||||
toplevel.title(title.clone());
|
||||
}
|
||||
if let Some(app_id) = &self.app_id {
|
||||
toplevel.app_id(app_id.clone());
|
||||
}
|
||||
|
||||
toplevel.done();
|
||||
|
||||
self.ext_list_instances.insert(toplevel);
|
||||
}
|
||||
|
||||
fn add_wlr_instance<D>(
|
||||
&mut self,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
@@ -292,7 +378,111 @@ impl ToplevelData {
|
||||
|
||||
toplevel.done();
|
||||
|
||||
self.instances.insert(toplevel, outputs);
|
||||
self.wlr_management_instances.insert(toplevel, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData, D>
|
||||
for ForeignToplevelManagerState
|
||||
where
|
||||
D: GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData>,
|
||||
D: Dispatch<ExtForeignToplevelListV1, ()>,
|
||||
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
handle: &DisplayHandle,
|
||||
client: &Client,
|
||||
resource: New<ExtForeignToplevelListV1>,
|
||||
_global_data: &ForeignToplevelGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(resource, ());
|
||||
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.add_ext_instance::<D>(handle, client, &manager);
|
||||
}
|
||||
|
||||
state.ext_list_instances.insert(manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ExtForeignToplevelListV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ExtForeignToplevelListV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ExtForeignToplevelListV1,
|
||||
request: <ExtForeignToplevelListV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
ext_foreign_toplevel_list_v1::Request::Stop => {
|
||||
resource.finished();
|
||||
|
||||
// remove the instance here so we won't send any more events.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.ext_list_instances.remove(resource);
|
||||
}
|
||||
ext_foreign_toplevel_list_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ExtForeignToplevelListV1,
|
||||
_data: &(),
|
||||
) {
|
||||
// also remove the instance here, in case `stop` was never sent, e.g. sudden disconnect.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.ext_list_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ExtForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
|
||||
where
|
||||
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
|
||||
D: ForeignToplevelHandler,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_resource: &ExtForeignToplevelHandleV1,
|
||||
request: <ExtForeignToplevelHandleV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
ext_foreign_toplevel_handle_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut D,
|
||||
_client: ClientId,
|
||||
resource: &ExtForeignToplevelHandleV1,
|
||||
_data: &(),
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.ext_list_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,10 +507,10 @@ where
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.add_instance::<D>(handle, client, &manager);
|
||||
data.add_wlr_instance::<D>(handle, client, &manager);
|
||||
}
|
||||
|
||||
state.instances.push(manager);
|
||||
state.wlr_management_instances.insert(manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||
@@ -346,8 +536,9 @@ where
|
||||
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
|
||||
resource.finished();
|
||||
|
||||
// remove the instance here so we won't send any more events.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
state.wlr_management_instances.remove(resource);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -359,8 +550,9 @@ where
|
||||
resource: &ZwlrForeignToplevelManagerV1,
|
||||
_data: &(),
|
||||
) {
|
||||
// also remove the instance here, in case `stop` was never sent, e.g. sudden disconnect.
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
state.instances.retain(|x| x != resource);
|
||||
state.wlr_management_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +575,7 @@ where
|
||||
let Some((surface, _)) = protocol_state
|
||||
.toplevels
|
||||
.iter()
|
||||
.find(|(_, data)| data.instances.contains_key(resource))
|
||||
.find(|(_, data)| data.wlr_management_instances.contains_key(resource))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -422,7 +614,7 @@ where
|
||||
) {
|
||||
let state = state.foreign_toplevel_manager_state();
|
||||
for data in state.toplevels.values_mut() {
|
||||
data.instances.retain(|instance, _| instance != resource);
|
||||
data.wlr_management_instances.remove(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +646,16 @@ fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3>
|
||||
#[macro_export]
|
||||
macro_rules! delegate_foreign_toplevel {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1: ()
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod gamma_control;
|
||||
pub mod mutter_x11_interop;
|
||||
pub mod output_management;
|
||||
pub mod screencopy;
|
||||
pub mod virtual_keyboard;
|
||||
pub mod virtual_pointer;
|
||||
|
||||
pub mod raw;
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
use smithay::backend::input::{
|
||||
Device, DeviceCapability, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, Keycode,
|
||||
UnusedEvent,
|
||||
};
|
||||
use smithay::delegate_virtual_keyboard_manager;
|
||||
use smithay::input::keyboard::xkb::ModMask;
|
||||
use smithay::input::keyboard::KeyboardHandle;
|
||||
use smithay::wayland::virtual_keyboard::VirtualKeyboardHandler;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct VirtualKeyboardInputBackend;
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct VirtualKeyboard;
|
||||
|
||||
impl Device for VirtualKeyboard {
|
||||
fn id(&self) -> String {
|
||||
String::from("virtual keyboard")
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
String::from("virtual keyboard")
|
||||
}
|
||||
|
||||
fn has_capability(&self, capability: DeviceCapability) -> bool {
|
||||
matches!(capability, DeviceCapability::Keyboard)
|
||||
}
|
||||
|
||||
fn usb_id(&self) -> Option<(u32, u32)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn syspath(&self) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualKeyboardKeyEvent {
|
||||
pub keycode: Keycode,
|
||||
pub state: KeyState,
|
||||
pub time: u32,
|
||||
}
|
||||
|
||||
impl Event<VirtualKeyboardInputBackend> for VirtualKeyboardKeyEvent {
|
||||
fn time(&self) -> u64 {
|
||||
self.time as u64 * 1000 // millis to micros
|
||||
}
|
||||
|
||||
fn device(&self) -> VirtualKeyboard {
|
||||
VirtualKeyboard
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardKeyEvent<VirtualKeyboardInputBackend> for VirtualKeyboardKeyEvent {
|
||||
fn key_code(&self) -> Keycode {
|
||||
self.keycode
|
||||
}
|
||||
|
||||
fn state(&self) -> KeyState {
|
||||
self.state
|
||||
}
|
||||
|
||||
fn count(&self) -> u32 {
|
||||
0 // Not used by niri
|
||||
}
|
||||
}
|
||||
|
||||
impl InputBackend for VirtualKeyboardInputBackend {
|
||||
type Device = VirtualKeyboard;
|
||||
|
||||
type KeyboardKeyEvent = VirtualKeyboardKeyEvent;
|
||||
type PointerAxisEvent = UnusedEvent;
|
||||
type PointerButtonEvent = UnusedEvent;
|
||||
type PointerMotionEvent = UnusedEvent;
|
||||
type PointerMotionAbsoluteEvent = UnusedEvent;
|
||||
|
||||
type GestureSwipeBeginEvent = UnusedEvent;
|
||||
type GestureSwipeUpdateEvent = UnusedEvent;
|
||||
type GestureSwipeEndEvent = UnusedEvent;
|
||||
type GesturePinchBeginEvent = UnusedEvent;
|
||||
type GesturePinchUpdateEvent = UnusedEvent;
|
||||
type GesturePinchEndEvent = UnusedEvent;
|
||||
type GestureHoldBeginEvent = UnusedEvent;
|
||||
type GestureHoldEndEvent = UnusedEvent;
|
||||
|
||||
type TouchDownEvent = UnusedEvent;
|
||||
type TouchUpEvent = UnusedEvent;
|
||||
type TouchMotionEvent = UnusedEvent;
|
||||
type TouchCancelEvent = UnusedEvent;
|
||||
type TouchFrameEvent = UnusedEvent;
|
||||
type TabletToolAxisEvent = UnusedEvent;
|
||||
type TabletToolProximityEvent = UnusedEvent;
|
||||
type TabletToolTipEvent = UnusedEvent;
|
||||
type TabletToolButtonEvent = UnusedEvent;
|
||||
|
||||
type SwitchToggleEvent = UnusedEvent;
|
||||
|
||||
type SpecialEvent = UnusedEvent;
|
||||
}
|
||||
|
||||
impl VirtualKeyboardHandler for State {
|
||||
fn on_keyboard_event(
|
||||
&mut self,
|
||||
keycode: Keycode,
|
||||
state: KeyState,
|
||||
time: u32,
|
||||
_keyboard: KeyboardHandle<Self>,
|
||||
) {
|
||||
// The virtual keyboard impl in Smithay changes the keymap, so we'll need to reset it on
|
||||
// the next real keyboard event.
|
||||
self.niri.reset_keymap = true;
|
||||
|
||||
let event = VirtualKeyboardKeyEvent {
|
||||
keycode,
|
||||
state,
|
||||
time,
|
||||
};
|
||||
self.process_input_event(InputEvent::<VirtualKeyboardInputBackend>::Keyboard { event });
|
||||
}
|
||||
|
||||
// We handle modifiers when the key event is sent.
|
||||
fn on_keyboard_modifiers(
|
||||
&mut self,
|
||||
_depressed_mods: ModMask,
|
||||
_latched_mods: ModMask,
|
||||
_locked_mods: ModMask,
|
||||
_keyboard: KeyboardHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
@@ -0,0 +1,332 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
||||
use smithay::wayland::compositor::{with_states, SurfaceData};
|
||||
use wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::handlers::background_effect::get_cached_blur_region;
|
||||
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, XrayPos};
|
||||
use crate::render_helpers::RenderCtx;
|
||||
use crate::utils::region::TransformedRegion;
|
||||
use crate::utils::surface_geo;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackgroundEffect {
|
||||
nonxray: FramebufferEffect,
|
||||
/// 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<f64>,
|
||||
pub saturation: Option<f64>,
|
||||
}
|
||||
|
||||
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<f64, Logical>,
|
||||
/// Effect subregion, will be clipped to `geometry`.
|
||||
///
|
||||
/// `subregion.iter()` should return `geometry`-relative rectangles.
|
||||
pub subregion: Option<TransformedRegion>,
|
||||
/// Geometry and radius for clipping in the same coordinate space as `geometry`.
|
||||
pub clip: Option<(Rectangle<f64, Logical>, CornerRadius)>,
|
||||
/// Scale to use for rounding to physical pixels.
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl RenderParams {
|
||||
fn fit_clip_radius(&mut self) {
|
||||
if let Some((geo, radius)) = &mut self.clip {
|
||||
// HACK: increase radius to avoid slight bleed on rounded corners.
|
||||
*radius = radius.expanded_by(1.);
|
||||
|
||||
*radius = radius.fit_to(geo.size.w as f32, geo.size.h as f32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
BackgroundEffectElement => {
|
||||
FramebufferEffect = FramebufferEffectElement,
|
||||
Xray = XrayElement,
|
||||
ExtraDamage = ExtraDamage,
|
||||
}
|
||||
}
|
||||
|
||||
impl BackgroundEffect {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nonxray: FramebufferEffect::new(),
|
||||
damage: ExtraDamage::new(),
|
||||
corner_radius: CornerRadius::default(),
|
||||
blur_config: niri_config::Blur::default(),
|
||||
options: Options::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Damage the background effect, for example when a blur subregion changes.
|
||||
pub fn damage(&mut self) {
|
||||
self.damage.damage_all();
|
||||
self.nonxray.damage();
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::Blur) {
|
||||
if self.blur_config == config {
|
||||
return;
|
||||
}
|
||||
|
||||
self.blur_config = config;
|
||||
self.damage.damage_all();
|
||||
self.nonxray.damage();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
corner_radius: CornerRadius,
|
||||
effect: niri_config::BackgroundEffect,
|
||||
has_blur_region: bool,
|
||||
) {
|
||||
// If the surface explicitly requests a blur region, default blur to true.
|
||||
let blur = if has_blur_region {
|
||||
effect.blur != Some(false)
|
||||
} else {
|
||||
effect.blur == Some(true)
|
||||
};
|
||||
|
||||
let mut options = Options {
|
||||
blur,
|
||||
xray: effect.xray == Some(true),
|
||||
noise: effect.noise,
|
||||
saturation: effect.saturation,
|
||||
};
|
||||
|
||||
// If we have some background effect but xray wasn't explicitly set, default it to true
|
||||
// since it's cheaper.
|
||||
if options.is_visible() && effect.xray.is_none() {
|
||||
options.xray = true;
|
||||
}
|
||||
|
||||
if self.options == options && self.corner_radius == corner_radius {
|
||||
return;
|
||||
}
|
||||
|
||||
self.options = options;
|
||||
self.corner_radius = corner_radius;
|
||||
self.damage.damage_all();
|
||||
self.nonxray.damage();
|
||||
}
|
||||
|
||||
pub fn is_visible(&self) -> bool {
|
||||
self.options.is_visible()
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
ctx: RenderCtx<GlesRenderer>,
|
||||
ns: Option<usize>,
|
||||
mut params: RenderParams,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(BackgroundEffectElement),
|
||||
) {
|
||||
if !self.is_visible() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(clip) = &mut params.clip {
|
||||
clip.1 = self.corner_radius;
|
||||
}
|
||||
params.fit_clip_radius();
|
||||
|
||||
let damage = self.damage.render(params.geometry);
|
||||
|
||||
// Use noise/saturation from options, falling back to blur defaults if blurred, and
|
||||
// to no effect if not blurred.
|
||||
let blur = self.options.blur && !self.blur_config.off;
|
||||
let blur_options = blur.then_some(BlurOptions::from(self.blur_config));
|
||||
let noise = if blur { self.blur_config.noise } else { 0. };
|
||||
let noise = self.options.noise.unwrap_or(noise) as f32;
|
||||
let saturation = if blur {
|
||||
self.blur_config.saturation
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
let saturation = self.options.saturation.unwrap_or(saturation) as f32;
|
||||
|
||||
if self.options.xray {
|
||||
let Some(xray) = ctx.xray else {
|
||||
return;
|
||||
};
|
||||
|
||||
push(damage.into());
|
||||
xray.render(
|
||||
ctx,
|
||||
params,
|
||||
xray_pos,
|
||||
blur,
|
||||
noise,
|
||||
saturation,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
} else {
|
||||
// Render non-xray effect.
|
||||
let elem = self
|
||||
.nonxray
|
||||
.render(ns, params, blur_options, noise, saturation);
|
||||
push(elem.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_params_for_tile(
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
scale: f64,
|
||||
clip_to_geometry: bool,
|
||||
block_out: bool,
|
||||
blur_region: Option<Arc<Vec<Rectangle<i32, Logical>>>>,
|
||||
surface_geo: Rectangle<f64, Logical>,
|
||||
surface_anim_scale: Scale<f64>,
|
||||
) -> Option<RenderParams> {
|
||||
// Effects not requested by the surface itself are drawn to match the geometry.
|
||||
let mut clip = true;
|
||||
|
||||
let mut effect_geometry = geometry;
|
||||
let mut subregion = None;
|
||||
if let Some(rects) = blur_region {
|
||||
if rects.is_empty() {
|
||||
// Surface has a set, but empty blur region.
|
||||
return None;
|
||||
} else {
|
||||
// If the surface itself requests the effects, apply different defaults.
|
||||
clip = clip_to_geometry;
|
||||
|
||||
// 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 block_out {
|
||||
clip = true;
|
||||
} else {
|
||||
let mut surface_geo = surface_geo.upscale(surface_anim_scale);
|
||||
surface_geo.loc += geometry.loc;
|
||||
|
||||
subregion = Some(TransformedRegion {
|
||||
rects,
|
||||
scale: surface_anim_scale,
|
||||
offset: surface_geo.loc,
|
||||
});
|
||||
|
||||
surface_geo = surface_geo
|
||||
.to_physical_precise_round(scale)
|
||||
.to_logical(scale);
|
||||
effect_geometry = surface_geo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This corner radius is reset to self.corner_radius in render().
|
||||
let clip = clip.then_some((geometry, CornerRadius::default()));
|
||||
|
||||
Some(RenderParams {
|
||||
geometry: effect_geometry,
|
||||
subregion,
|
||||
clip,
|
||||
scale,
|
||||
})
|
||||
}
|
||||
|
||||
/// Per-surface background effect stored in its data map.
|
||||
struct SurfaceBackgroundEffect(Mutex<BackgroundEffect>);
|
||||
|
||||
impl SurfaceBackgroundEffect {
|
||||
fn get(states: &SurfaceData) -> &Self {
|
||||
states
|
||||
.data_map
|
||||
.get_or_insert(|| SurfaceBackgroundEffect(Mutex::new(BackgroundEffect::new())))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn damage_surface(states: &SurfaceData) {
|
||||
if let Some(effect) = states.data_map.get::<SurfaceBackgroundEffect>() {
|
||||
effect.0.lock().unwrap().damage();
|
||||
}
|
||||
}
|
||||
|
||||
// Silence, Clippy
|
||||
// A Smithay user is talking
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_for_tile(
|
||||
ctx: RenderCtx<GlesRenderer>,
|
||||
ns: Option<usize>,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
scale: f64,
|
||||
clip_to_geometry: bool,
|
||||
surface: &WlSurface,
|
||||
surface_off: Point<f64, Logical>,
|
||||
surface_anim_scale: Scale<f64>,
|
||||
blur_config: niri_config::Blur,
|
||||
radius: CornerRadius,
|
||||
effect: niri_config::BackgroundEffect,
|
||||
should_block_out: bool,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(BackgroundEffectElement),
|
||||
) {
|
||||
with_states(surface, |states| {
|
||||
let background_effect = SurfaceBackgroundEffect::get(states);
|
||||
let mut background_effect = background_effect.0.lock().unwrap();
|
||||
|
||||
let blur_region = get_cached_blur_region(states);
|
||||
let has_blur_region = blur_region.as_ref().is_some_and(|r| !r.is_empty());
|
||||
|
||||
background_effect.update_config(blur_config);
|
||||
background_effect.update_render_elements(radius, effect, has_blur_region);
|
||||
|
||||
if !background_effect.is_visible() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut surface_geo = surface_geo(states).unwrap_or_default().to_f64();
|
||||
surface_geo.loc += surface_off;
|
||||
|
||||
let Some(params) = render_params_for_tile(
|
||||
geometry,
|
||||
scale,
|
||||
clip_to_geometry,
|
||||
should_block_out,
|
||||
blur_region,
|
||||
surface_geo,
|
||||
surface_anim_scale,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let xray_pos = xray_pos.offset(params.geometry.loc - geometry.loc);
|
||||
background_effect.render(ctx, ns, params, xray_pos, push);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
use std::cmp::max;
|
||||
use std::iter::{once, zip};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::{ensure, Context as _};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::gles::{ffi, link_program, GlesError, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{ContextId, Renderer as _, Texture as _};
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::{Buffer, Size};
|
||||
|
||||
use crate::render_helpers::shaders::Shaders;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Blur {
|
||||
program: BlurProgram,
|
||||
/// Context ID of the renderer that created the program and the textures.
|
||||
renderer_context_id: ContextId<GlesTexture>,
|
||||
/// Output texture followed by intermediate textures, large to small.
|
||||
///
|
||||
/// Created lazily and stored here to avoid recreating blur textures frequently.
|
||||
textures: Vec<GlesTexture>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BlurOptions {
|
||||
pub passes: u8,
|
||||
pub offset: f64,
|
||||
}
|
||||
|
||||
impl From<niri_config::Blur> for BlurOptions {
|
||||
fn from(config: niri_config::Blur) -> Self {
|
||||
Self {
|
||||
passes: config.passes,
|
||||
offset: config.offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlurProgram(Rc<BlurProgramInner>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BlurProgramInner {
|
||||
down: BlurProgramInternal,
|
||||
up: BlurProgramInternal,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BlurProgramInternal {
|
||||
program: ffi::types::GLuint,
|
||||
uniform_tex: ffi::types::GLint,
|
||||
uniform_half_pixel: ffi::types::GLint,
|
||||
uniform_offset: ffi::types::GLint,
|
||||
attrib_vert: ffi::types::GLint,
|
||||
}
|
||||
|
||||
unsafe fn compile_program(gl: &ffi::Gles2, src: &str) -> Result<BlurProgramInternal, GlesError> {
|
||||
let program = unsafe { link_program(gl, include_str!("shaders/blur.vert"), src)? };
|
||||
|
||||
let vert = c"vert";
|
||||
let tex = c"tex";
|
||||
let half_pixel = c"half_pixel";
|
||||
let offset = c"offset";
|
||||
|
||||
Ok(BlurProgramInternal {
|
||||
program,
|
||||
uniform_tex: gl.GetUniformLocation(program, tex.as_ptr()),
|
||||
uniform_half_pixel: gl.GetUniformLocation(program, half_pixel.as_ptr()),
|
||||
uniform_offset: gl.GetUniformLocation(program, offset.as_ptr()),
|
||||
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()),
|
||||
})
|
||||
}
|
||||
|
||||
impl BlurProgram {
|
||||
pub fn compile(renderer: &mut GlesRenderer) -> anyhow::Result<Self> {
|
||||
renderer
|
||||
.with_context(move |gl| unsafe {
|
||||
let down = compile_program(gl, include_str!("shaders/blur_down.frag"))
|
||||
.context("error compiling blur_down shader")?;
|
||||
let up = compile_program(gl, include_str!("shaders/blur_up.frag"))
|
||||
.context("error compiling blur_up shader")?;
|
||||
Ok(Self(Rc::new(BlurProgramInner { down, up })))
|
||||
})
|
||||
.context("error making GL context current")?
|
||||
}
|
||||
|
||||
pub fn destroy(self, renderer: &mut GlesRenderer) -> Result<(), GlesError> {
|
||||
renderer.with_context(move |gl| unsafe {
|
||||
gl.DeleteProgram(self.0.down.program);
|
||||
gl.DeleteProgram(self.0.up.program);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Blur {
|
||||
pub fn new(renderer: &mut GlesRenderer) -> Option<Self> {
|
||||
let program = Shaders::get(renderer).blur.clone()?;
|
||||
Some(Self {
|
||||
program,
|
||||
renderer_context_id: renderer.context_id(),
|
||||
textures: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn context_id(&self) -> ContextId<GlesTexture> {
|
||||
self.renderer_context_id.clone()
|
||||
}
|
||||
|
||||
pub fn prepare_textures(
|
||||
&mut self,
|
||||
mut create_texture: impl FnMut(Fourcc, Size<i32, Buffer>) -> Result<GlesTexture, GlesError>,
|
||||
source: &GlesTexture,
|
||||
options: BlurOptions,
|
||||
) -> anyhow::Result<()> {
|
||||
let _span = tracy_client::span!("Blur::prepare_textures");
|
||||
|
||||
let passes = options.passes.clamp(1, 31) as usize;
|
||||
let size = source.size();
|
||||
|
||||
if let Some(output) = self.textures.first_mut() {
|
||||
let old_size = output.size();
|
||||
if old_size != size {
|
||||
trace!(
|
||||
"recreating textures: output size changed from {} × {} to {} × {}",
|
||||
old_size.w,
|
||||
old_size.h,
|
||||
size.w,
|
||||
size.h
|
||||
);
|
||||
self.textures.clear();
|
||||
} else if !output.is_unique_reference() {
|
||||
debug!("recreating textures: not unique",);
|
||||
// We only need to recreate the output texture here, but this case shouldn't really
|
||||
// happen anyway, and this is simpler.
|
||||
self.textures.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Create any missing textures.
|
||||
let mut w = size.w;
|
||||
let mut h = size.h;
|
||||
for i in 0..=passes {
|
||||
let size = Size::new(w, h);
|
||||
w = max(1, w / 2);
|
||||
h = max(1, h / 2);
|
||||
|
||||
if self.textures.len() > i {
|
||||
// This texture already exists.
|
||||
continue;
|
||||
}
|
||||
|
||||
// debug!("creating texture for step {i} sized {w} × {h}");
|
||||
|
||||
let texture: GlesTexture =
|
||||
create_texture(Fourcc::Abgr8888, size).context("error creating texture")?;
|
||||
self.textures.push(texture);
|
||||
}
|
||||
|
||||
// Drop any no longer needed textures.
|
||||
self.textures.drain(passes + 1..);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
source: &GlesTexture,
|
||||
options: BlurOptions,
|
||||
) -> anyhow::Result<GlesTexture> {
|
||||
let _span = tracy_client::span!("Blur::render");
|
||||
trace!("rendering blur");
|
||||
|
||||
ensure!(
|
||||
renderer.context_id() == self.renderer_context_id,
|
||||
"wrong renderer"
|
||||
);
|
||||
|
||||
let passes = options.passes.clamp(1, 31) as usize;
|
||||
let size = source.size();
|
||||
|
||||
ensure!(
|
||||
self.textures.len() == passes + 1,
|
||||
"wrong textures len: expected {}, got {}",
|
||||
passes + 1,
|
||||
self.textures.len()
|
||||
);
|
||||
|
||||
let output = &mut self.textures[0];
|
||||
ensure!(
|
||||
output.size() == size,
|
||||
"wrong output texture size: expected {size:?}, got {:?}",
|
||||
output.size()
|
||||
);
|
||||
|
||||
ensure!(
|
||||
output.is_unique_reference(),
|
||||
"output texture has a non-unique reference"
|
||||
);
|
||||
|
||||
renderer.with_profiled_context(gpu_span_location!("Blur::render"), |gl| unsafe {
|
||||
while gl.GetError() != ffi::NO_ERROR {}
|
||||
|
||||
gl.Disable(ffi::BLEND);
|
||||
gl.Disable(ffi::SCISSOR_TEST);
|
||||
|
||||
gl.ActiveTexture(ffi::TEXTURE0);
|
||||
|
||||
let mut fbos = [0; 2];
|
||||
gl.GenFramebuffers(fbos.len() as _, fbos.as_mut_ptr());
|
||||
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbos[0]);
|
||||
|
||||
let program = &self.program.0.down;
|
||||
gl.UseProgram(program.program);
|
||||
gl.Uniform1i(program.uniform_tex, 0);
|
||||
gl.Uniform1f(program.uniform_offset, options.offset as f32);
|
||||
|
||||
let vertices: [f32; 12] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0];
|
||||
gl.EnableVertexAttribArray(program.attrib_vert as u32);
|
||||
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
|
||||
gl.VertexAttribPointer(
|
||||
program.attrib_vert as u32,
|
||||
2,
|
||||
ffi::FLOAT,
|
||||
ffi::FALSE,
|
||||
0,
|
||||
vertices.as_ptr().cast(),
|
||||
);
|
||||
|
||||
let src = once(source).chain(&self.textures[1..]);
|
||||
let dst = &self.textures[1..];
|
||||
for (src, dst) in zip(src, dst) {
|
||||
let dst_size = dst.size();
|
||||
let w = dst_size.w;
|
||||
let h = dst_size.h;
|
||||
gl.Viewport(0, 0, w, h);
|
||||
|
||||
// During downsampling, half_pixel is half of the destination pixel.
|
||||
gl.Uniform2f(program.uniform_half_pixel, 0.5 / w as f32, 0.5 / h as f32);
|
||||
|
||||
let src = src.tex_id();
|
||||
let dst = dst.tex_id();
|
||||
|
||||
trace!("drawing down {src} to {dst}");
|
||||
gl.FramebufferTexture2D(
|
||||
ffi::DRAW_FRAMEBUFFER,
|
||||
ffi::COLOR_ATTACHMENT0,
|
||||
ffi::TEXTURE_2D,
|
||||
dst,
|
||||
0,
|
||||
);
|
||||
|
||||
gl.BindTexture(ffi::TEXTURE_2D, src);
|
||||
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
|
||||
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
|
||||
gl.TexParameteri(
|
||||
ffi::TEXTURE_2D,
|
||||
ffi::TEXTURE_WRAP_S,
|
||||
ffi::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
gl.TexParameteri(
|
||||
ffi::TEXTURE_2D,
|
||||
ffi::TEXTURE_WRAP_T,
|
||||
ffi::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
|
||||
gl.DrawArrays(ffi::TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
gl.DisableVertexAttribArray(program.attrib_vert as u32);
|
||||
|
||||
// Up
|
||||
let program = &self.program.0.up;
|
||||
gl.UseProgram(program.program);
|
||||
gl.Uniform1i(program.uniform_tex, 0);
|
||||
gl.Uniform1f(program.uniform_offset, options.offset as f32);
|
||||
|
||||
let vertices: [f32; 12] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0];
|
||||
gl.EnableVertexAttribArray(program.attrib_vert as u32);
|
||||
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
|
||||
gl.VertexAttribPointer(
|
||||
program.attrib_vert as u32,
|
||||
2,
|
||||
ffi::FLOAT,
|
||||
ffi::FALSE,
|
||||
0,
|
||||
vertices.as_ptr().cast(),
|
||||
);
|
||||
|
||||
let src = self.textures.iter().rev();
|
||||
let dst = self.textures.iter().rev().skip(1);
|
||||
for (src, dst) in zip(src, dst) {
|
||||
let dst_size = dst.size();
|
||||
let w = dst_size.w;
|
||||
let h = dst_size.h;
|
||||
gl.Viewport(0, 0, w, h);
|
||||
|
||||
// During upsampling, half_pixel is half of the source pixel.
|
||||
let src_size = src.size();
|
||||
let src_w = src_size.w as f32;
|
||||
let src_h = src_size.h as f32;
|
||||
gl.Uniform2f(program.uniform_half_pixel, 0.5 / src_w, 0.5 / src_h);
|
||||
|
||||
let src = src.tex_id();
|
||||
let dst = dst.tex_id();
|
||||
|
||||
trace!("drawing up {src} to {dst}");
|
||||
gl.FramebufferTexture2D(
|
||||
ffi::DRAW_FRAMEBUFFER,
|
||||
ffi::COLOR_ATTACHMENT0,
|
||||
ffi::TEXTURE_2D,
|
||||
dst,
|
||||
0,
|
||||
);
|
||||
|
||||
gl.BindTexture(ffi::TEXTURE_2D, src);
|
||||
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
|
||||
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
|
||||
gl.TexParameteri(
|
||||
ffi::TEXTURE_2D,
|
||||
ffi::TEXTURE_WRAP_S,
|
||||
ffi::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
gl.TexParameteri(
|
||||
ffi::TEXTURE_2D,
|
||||
ffi::TEXTURE_WRAP_T,
|
||||
ffi::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
|
||||
gl.DrawArrays(ffi::TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
gl.DisableVertexAttribArray(program.attrib_vert as u32);
|
||||
|
||||
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, 0);
|
||||
gl.DeleteFramebuffers(fbos.len() as _, fbos.as_ptr());
|
||||
})?;
|
||||
|
||||
Ok(self.textures[0].clone())
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, Unde
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::NiriRenderer;
|
||||
@@ -285,6 +286,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
let _span = tracy_client::span!("BorderRenderElement::draw");
|
||||
frame.with_gpu_span(gpu_span_location!("BorderRenderElement::draw"), |frame| {
|
||||
@@ -295,6 +297,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -312,9 +315,10 @@ impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions, cache)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::backend::renderer::buffer_y_inverted;
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{
|
||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
|
||||
};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::damage::ExtraDamage;
|
||||
@@ -74,12 +76,18 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
* Mat3::from_cols_array(transform.matrix().as_ref())
|
||||
* Mat3::from_translation(-Vec2::new(0.5, 0.5));
|
||||
|
||||
// FIXME: y_inverted
|
||||
let y_invert = if buffer_y_inverted(self.inner.buffer()).unwrap_or(false) {
|
||||
Mat3::from_scale(Vec2::new(1., -1.))
|
||||
} else {
|
||||
Mat3::IDENTITY
|
||||
};
|
||||
|
||||
let input_to_geo = transform_matrix * Mat3::from_scale(elem_geo_size / geo_size)
|
||||
* Mat3::from_translation((elem_geo_loc - geo_loc) / elem_geo_size)
|
||||
// Apply viewporter src.
|
||||
* Mat3::from_scale(buf_size / src_size)
|
||||
* Mat3::from_translation(-src_loc / buf_size);
|
||||
* Mat3::from_translation(-src_loc / buf_size)
|
||||
* y_invert;
|
||||
|
||||
let geo_size = (self.geometry.size.w as f32, self.geometry.size.h as f32);
|
||||
|
||||
@@ -228,9 +236,18 @@ impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
frame.override_default_tex_program(self.program.clone(), self.compute_uniforms());
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.inner,
|
||||
frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
frame.clear_tex_program_override();
|
||||
Ok(())
|
||||
}
|
||||
@@ -252,11 +269,12 @@ impl<'render> RenderElement<TtyRenderer<'render>>
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
frame
|
||||
.as_gles_frame()
|
||||
.override_default_tex_program(self.program.clone(), self.compute_uniforms());
|
||||
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions, cache)?;
|
||||
frame.as_gles_frame().clear_tex_program_override();
|
||||
Ok(())
|
||||
}
|
||||
@@ -272,10 +290,6 @@ impl<'render> RenderElement<TtyRenderer<'render>>
|
||||
}
|
||||
|
||||
impl RoundedCornerDamage {
|
||||
pub fn set_size(&mut self, size: Size<f64, Logical>) {
|
||||
self.damage.set_size(size);
|
||||
}
|
||||
|
||||
pub fn set_corner_radius(&mut self, corner_radius: CornerRadius) {
|
||||
if self.corner_radius == corner_radius {
|
||||
return;
|
||||
@@ -286,7 +300,7 @@ impl RoundedCornerDamage {
|
||||
self.damage.damage_all();
|
||||
}
|
||||
|
||||
pub fn element(&self) -> ExtraDamage {
|
||||
self.damage.clone()
|
||||
pub fn render(&self, geometry: Rectangle<f64, Logical>) -> ExtraDamage {
|
||||
self.damage.render(geometry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use smithay::backend::renderer::element::{Element, Id, RenderElement};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::Renderer;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtraDamage {
|
||||
@@ -19,22 +20,14 @@ impl ExtraDamage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, size: Size<f64, Logical>) {
|
||||
if self.geometry.size == size {
|
||||
return;
|
||||
}
|
||||
|
||||
self.geometry.size = size;
|
||||
self.commit.increment();
|
||||
}
|
||||
|
||||
pub fn damage_all(&mut self) {
|
||||
self.commit.increment();
|
||||
}
|
||||
|
||||
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
|
||||
self.geometry.loc = location;
|
||||
self
|
||||
pub fn render(&self, geometry: Rectangle<f64, Logical>) -> Self {
|
||||
let mut this = self.clone();
|
||||
this.geometry = geometry;
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +63,7 @@ impl<R: Renderer> RenderElement<R> for ExtraDamage {
|
||||
_dst: Rectangle<i32, Physical>,
|
||||
_damage: &[Rectangle<i32, Physical>],
|
||||
_opaque_regions: &[Rectangle<i32, Physical>],
|
||||
_cache: Option<&UserDataMap>,
|
||||
) -> Result<(), R::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
use std::mem;
|
||||
|
||||
use anyhow::{ensure, Context as _};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::{Id, RenderElementStates};
|
||||
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{
|
||||
Bind as _, Color32F, ContextId, FrameContext as _, Offscreen as _, Renderer as _, Texture,
|
||||
};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Scale, Size, Transform};
|
||||
|
||||
use crate::niri::OutputRenderElements;
|
||||
use crate::render_helpers::blur::{Blur, BlurOptions};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EffectBuffer {
|
||||
/// Id to be used for this effect buffer's elements.
|
||||
id: Id,
|
||||
|
||||
/// Size of the effect buffer.
|
||||
size: Size<i32, Buffer>,
|
||||
/// Scale of the effect buffer.
|
||||
scale: Scale<f64>,
|
||||
/// Options for blurring.
|
||||
blur_options: BlurOptions,
|
||||
|
||||
/// Elements to be rendered on demand.
|
||||
elements: Elements,
|
||||
/// Offscreen buffer where elements get rendered.
|
||||
offscreen: Option<Offscreen>,
|
||||
/// Blurring program, if available.
|
||||
blur: Option<Blur>,
|
||||
|
||||
/// Commit counter that takes into account both original and blurred texture changes.
|
||||
commit_counter: CommitCounter,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Elements {
|
||||
/// Contents remain unchanged.
|
||||
Unchanged(
|
||||
// Storage to avoid reallocating it every time.
|
||||
Vec<OutputRenderElements<GlesRenderer>>,
|
||||
),
|
||||
/// New contents, need to check damage and render.
|
||||
New(Vec<OutputRenderElements<GlesRenderer>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Offscreen {
|
||||
/// The texture with the offscreen contents.
|
||||
texture: GlesTexture,
|
||||
/// Id of the renderer context that the texture comes from.
|
||||
renderer_context_id: ContextId<GlesTexture>,
|
||||
/// Scale of the texture.
|
||||
scale: Scale<f64>,
|
||||
/// Damage tracker for drawing to the texture.
|
||||
damage: OutputDamageTracker,
|
||||
/// Render element states from the last render into the offscreen.
|
||||
states: RenderElementStates,
|
||||
/// Rendered blurred version of the texture.
|
||||
///
|
||||
/// When texture needs to be reblurred, this field must be reset to `None`.
|
||||
blurred: Option<GlesTexture>,
|
||||
}
|
||||
|
||||
impl Default for Elements {
|
||||
fn default() -> Self {
|
||||
Self::Unchanged(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: Id::new(),
|
||||
size: Size::default(),
|
||||
scale: Scale::from(1.),
|
||||
blur_options: BlurOptions::default(),
|
||||
elements: Elements::default(),
|
||||
offscreen: None,
|
||||
blur: None,
|
||||
commit_counter: CommitCounter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &Id {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn commit(&self) -> CommitCounter {
|
||||
self.commit_counter
|
||||
}
|
||||
|
||||
pub fn logical_size(&self) -> Size<f64, Logical> {
|
||||
self.size.to_f64().to_logical(self.scale, Transform::Normal)
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> Scale<f64> {
|
||||
self.scale
|
||||
}
|
||||
|
||||
pub fn render_element_states(&self) -> Option<&RenderElementStates> {
|
||||
self.offscreen.as_ref().map(|o| &o.states)
|
||||
}
|
||||
|
||||
pub fn update_size(&mut self, size: Size<i32, Physical>, scale: Scale<f64>) {
|
||||
self.size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
self.scale = scale;
|
||||
}
|
||||
|
||||
pub fn update_blur_options(&mut self, options: BlurOptions) {
|
||||
if self.blur_options == options {
|
||||
return;
|
||||
}
|
||||
|
||||
self.blur_options = options;
|
||||
|
||||
if let Some(offscreen) = &mut self.offscreen {
|
||||
if offscreen.blurred.is_some() {
|
||||
offscreen.blurred = None;
|
||||
self.commit_counter.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elements(&mut self) -> &mut Vec<OutputRenderElements<GlesRenderer>> {
|
||||
// Assume we're going to insert new elements, switch to New.
|
||||
match mem::take(&mut self.elements) {
|
||||
Elements::Unchanged(elements) | Elements::New(elements) => {
|
||||
self.elements = Elements::New(elements);
|
||||
}
|
||||
}
|
||||
let Elements::New(elements) = &mut self.elements else {
|
||||
unreachable!();
|
||||
};
|
||||
elements
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, renderer: &mut GlesRenderer, blur: bool) -> bool {
|
||||
if let Err(err) = self.prepare_offscreen(renderer) {
|
||||
warn!("error preparing offscreen: {err:?}");
|
||||
return false;
|
||||
};
|
||||
|
||||
if blur {
|
||||
if let Err(err) = self.prepare_blur(renderer) {
|
||||
warn!("error preparing blur: {err:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn prepare_offscreen(&mut self, renderer: &mut GlesRenderer) -> anyhow::Result<()> {
|
||||
let _span = tracy_client::span!("EffectBuffer::prepare_offscreen");
|
||||
|
||||
// Check if we need to create or recreate the texture.
|
||||
let size_string;
|
||||
let mut reason = "";
|
||||
if let Some(Offscreen {
|
||||
texture,
|
||||
renderer_context_id,
|
||||
..
|
||||
}) = &mut self.offscreen
|
||||
{
|
||||
let old_size = texture.size();
|
||||
if old_size != self.size {
|
||||
size_string = format!(
|
||||
"size changed from {} × {} to {} × {}",
|
||||
old_size.w, old_size.h, self.size.w, self.size.h
|
||||
);
|
||||
reason = &size_string;
|
||||
|
||||
self.offscreen = None;
|
||||
} else if !texture.is_unique_reference() {
|
||||
reason = "not unique";
|
||||
|
||||
self.offscreen = None;
|
||||
} else if *renderer_context_id != renderer.context_id() {
|
||||
reason = "renderer id changed";
|
||||
|
||||
self.offscreen = None;
|
||||
}
|
||||
} else {
|
||||
reason = "first render";
|
||||
}
|
||||
|
||||
let offscreen = if let Some(offscreen) = &mut self.offscreen {
|
||||
offscreen
|
||||
} else {
|
||||
trace!("creating new offscreen texture: {reason}");
|
||||
let span = tracy_client::span!("creating effect offscreen texture");
|
||||
span.emit_text(reason);
|
||||
|
||||
let texture: GlesTexture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, self.size)
|
||||
.context("error creating texture")?;
|
||||
|
||||
let buffer_size = self.size.to_logical(1, Transform::Normal).to_physical(1);
|
||||
let damage = OutputDamageTracker::new(buffer_size, self.scale, Transform::Normal);
|
||||
|
||||
self.offscreen.insert(Offscreen {
|
||||
texture,
|
||||
renderer_context_id: renderer.context_id(),
|
||||
scale: self.scale,
|
||||
damage,
|
||||
states: RenderElementStates::default(),
|
||||
blurred: None,
|
||||
})
|
||||
};
|
||||
|
||||
// Recreate the damage tracker if the scale changes. We already recreate it for buffer size
|
||||
// changes, and transform is always Normal.
|
||||
if offscreen.scale != self.scale {
|
||||
offscreen.scale = self.scale;
|
||||
|
||||
trace!("recreating damage tracker due to scale change");
|
||||
let buffer_size = self.size.to_logical(1, Transform::Normal).to_physical(1);
|
||||
offscreen.damage = OutputDamageTracker::new(buffer_size, self.scale, Transform::Normal);
|
||||
|
||||
self.commit_counter.increment();
|
||||
offscreen.blurred = None;
|
||||
}
|
||||
|
||||
// Render the elements if any.
|
||||
let mut elements = match mem::take(&mut self.elements) {
|
||||
Elements::New(elements) => elements,
|
||||
x @ Elements::Unchanged(_) => {
|
||||
// No redrawing necessary.
|
||||
self.elements = x;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let res = {
|
||||
let mut target = renderer
|
||||
.bind(&mut offscreen.texture)
|
||||
.context("error binding texture")?;
|
||||
offscreen
|
||||
.damage
|
||||
.render_output(renderer, &mut target, 1, &elements, Color32F::TRANSPARENT)
|
||||
.context("error rendering")?
|
||||
};
|
||||
|
||||
offscreen.states = res.states;
|
||||
|
||||
if res.damage.is_some() {
|
||||
self.commit_counter.increment();
|
||||
|
||||
// Original texture changed; reset the blurred texture.
|
||||
offscreen.blurred = None;
|
||||
}
|
||||
|
||||
// Clear and put the storage back.
|
||||
elements.clear();
|
||||
self.elements = Elements::Unchanged(elements);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_blur(&mut self, renderer: &mut GlesRenderer) -> anyhow::Result<()> {
|
||||
let offscreen = self.offscreen.as_mut().context("missing offscreen")?;
|
||||
if offscreen.blurred.is_some() {
|
||||
// Already rendered.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(blur) = &self.blur {
|
||||
if blur.context_id() != renderer.context_id() {
|
||||
debug!("recreating blur: renderer changed");
|
||||
self.blur = None;
|
||||
}
|
||||
}
|
||||
|
||||
let blur = if let Some(blur) = &mut self.blur {
|
||||
blur
|
||||
} else {
|
||||
let Some(blur) = Blur::new(renderer) else {
|
||||
// Missing blur shader.
|
||||
return Ok(());
|
||||
};
|
||||
self.blur.insert(blur)
|
||||
};
|
||||
|
||||
ensure!(
|
||||
offscreen.renderer_context_id == renderer.context_id(),
|
||||
"wrong renderer context id"
|
||||
);
|
||||
|
||||
blur.prepare_textures(
|
||||
|fourcc, size| renderer.create_buffer(fourcc, size),
|
||||
&offscreen.texture,
|
||||
self.blur_options,
|
||||
)
|
||||
.context("error preparing blur textures")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render(&mut self, frame: &mut GlesFrame, blur: bool) -> anyhow::Result<GlesTexture> {
|
||||
let offscreen = self.offscreen.as_mut().context("offscreen is missing")?;
|
||||
|
||||
if !blur {
|
||||
return Ok(offscreen.texture.clone());
|
||||
}
|
||||
|
||||
let texture = if let Some(texture) = &offscreen.blurred {
|
||||
texture.clone()
|
||||
} else {
|
||||
let blur = self.blur.as_mut().context("blur is missing")?;
|
||||
let mut guard = frame.renderer();
|
||||
let renderer = guard.as_mut();
|
||||
let blurred = blur
|
||||
.render(renderer, &offscreen.texture, self.blur_options)
|
||||
.context("error rendering blur")?;
|
||||
offscreen.blurred.insert(blurred).clone()
|
||||
};
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::{Element, Id, RenderElement};
|
||||
use smithay::backend::renderer::gles::{
|
||||
ffi, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform,
|
||||
};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{Frame as _, FrameContext, Offscreen, Texture as _};
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
use crate::render_helpers::background_effect::RenderParams;
|
||||
use crate::render_helpers::blur::{Blur, BlurOptions};
|
||||
use crate::render_helpers::renderer::AsGlesFrame as _;
|
||||
use crate::render_helpers::shaders::{mat3_uniform, Shaders};
|
||||
use crate::utils::region::TransformedRegion;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FramebufferEffect {
|
||||
id: Id,
|
||||
commit: CommitCounter,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FramebufferEffectElement {
|
||||
id: Id,
|
||||
commit: CommitCounter,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
clip_geo: Rectangle<f64, Logical>,
|
||||
corner_radius: CornerRadius,
|
||||
subregion: Option<TransformedRegion>,
|
||||
scale: f32,
|
||||
blur_options: Option<BlurOptions>,
|
||||
noise: f32,
|
||||
saturation: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
framebuffer: Option<GlesTexture>,
|
||||
blur: Option<Blur>,
|
||||
intermediate: Option<GlesTexture>,
|
||||
/// Reusable storage for subregion-filtered damage rects.
|
||||
subregion_damage: Vec<Rectangle<i32, Physical>>,
|
||||
}
|
||||
|
||||
impl FramebufferEffect {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: Id::new(),
|
||||
commit: CommitCounter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn damage(&mut self) {
|
||||
self.commit.increment();
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
ns: Option<usize>,
|
||||
params: RenderParams,
|
||||
blur_options: Option<BlurOptions>,
|
||||
noise: f32,
|
||||
saturation: f32,
|
||||
) -> FramebufferEffectElement {
|
||||
let (clip_geo, corner_radius) = params
|
||||
.clip
|
||||
.unwrap_or((params.geometry, CornerRadius::default()));
|
||||
|
||||
let mut id = self.id.clone();
|
||||
if let Some(ns) = ns {
|
||||
id = id.namespaced(ns);
|
||||
}
|
||||
|
||||
FramebufferEffectElement {
|
||||
id,
|
||||
commit: self.commit,
|
||||
geometry: params.geometry,
|
||||
clip_geo,
|
||||
corner_radius,
|
||||
subregion: params.subregion,
|
||||
scale: params.scale as f32,
|
||||
blur_options,
|
||||
noise,
|
||||
saturation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FramebufferEffectElement {
|
||||
fn compute_uniforms(
|
||||
&self,
|
||||
crop: Rectangle<f64, Logical>,
|
||||
transform: Transform,
|
||||
) -> [Uniform<'static>; 7] {
|
||||
let offset = crop.loc - (self.clip_geo.loc - self.geometry.loc);
|
||||
let offset = Vec2::new(offset.x as f32, offset.y as f32);
|
||||
let crop_size = Vec2::new(crop.size.w as f32, crop.size.h as f32);
|
||||
let clip_size = Vec2::new(self.clip_geo.size.w as f32, self.clip_geo.size.h as f32);
|
||||
|
||||
// Our v_coords are [0, 1] inside crop. We want them to be [0, 1] inside clip_geo.
|
||||
let input_to_clip_geo =
|
||||
Mat3::from_scale(crop_size / clip_size) * Mat3::from_translation(offset / crop_size);
|
||||
|
||||
// Revert the effect of the texture transform.
|
||||
let transform_mat = Mat3::from_translation(Vec2::new(0.5, 0.5))
|
||||
* Mat3::from_cols_array(transform.matrix().as_ref())
|
||||
* Mat3::from_translation(Vec2::new(-0.5, -0.5));
|
||||
let input_to_clip_geo = input_to_clip_geo * transform_mat;
|
||||
|
||||
let clip_geo_size = (self.clip_geo.size.w as f32, self.clip_geo.size.h as f32);
|
||||
|
||||
[
|
||||
Uniform::new("niri_scale", self.scale),
|
||||
Uniform::new("geo_size", clip_geo_size),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
|
||||
mat3_uniform("input_to_geo", input_to_clip_geo),
|
||||
Uniform::new("noise", self.noise),
|
||||
Uniform::new("saturation", self.saturation),
|
||||
Uniform::new("bg_color", [0f32, 0., 0., 0.]),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FramebufferEffectElement {
|
||||
fn id(&self) -> &Id {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.commit
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
// We don't use src for drawing but we can use it to figure out how we were cropped.
|
||||
let size = self.geometry.size.to_buffer(1., Transform::Normal);
|
||||
Rectangle::from_size(size)
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.geometry.to_physical_precise_round(scale)
|
||||
}
|
||||
|
||||
fn is_framebuffer_effect(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for FramebufferEffectElement {
|
||||
fn capture_framebuffer(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
cache: &UserDataMap,
|
||||
) -> Result<(), GlesError> {
|
||||
let _span = tracy_client::span!("FramebufferEffectElement::capture_framebuffer");
|
||||
let location = gpu_span_location!("FramebufferEffectElement::capture_framebuffer");
|
||||
frame.with_gpu_span(location, |frame| {
|
||||
let output_rect = Rectangle::from_size(frame.output_size());
|
||||
let transform = frame.transformation();
|
||||
|
||||
let mut guard = frame.renderer();
|
||||
|
||||
let inner = cache
|
||||
.get_or_insert::<RefCell<Inner>, _>(|| RefCell::new(Inner::new(guard.as_mut())));
|
||||
let mut inner = inner.borrow_mut();
|
||||
let inner = &mut *inner;
|
||||
|
||||
inner.intermediate = None;
|
||||
|
||||
// We want clamp-to-edge behavior for out-of-bounds pixels. However, glBlitFramebuffer
|
||||
// seems to skip out-of-bounds pixels, even though my reading of the docs suggests
|
||||
// otherwise (we use GL_LINEAR filter). So, clamp dst to the framebuffer bounds
|
||||
// ourselves.
|
||||
let clamped_dst = match dst.intersection(output_rect) {
|
||||
Some(clamped) => clamped,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let clamp_scale = clamped_dst.size.to_f64() / dst.size.to_f64();
|
||||
|
||||
let dst = transform.transform_rect_in(clamped_dst, &output_rect.size);
|
||||
|
||||
// Compute size from our geometry and scale.
|
||||
//
|
||||
// The "correct" size is always dst.size since that's the pixel region we're actually
|
||||
// blitting. However, using dst.size causes two undesirable things when zooming out for
|
||||
// the overview:
|
||||
// 1. dst.size shrinks every frame, causing a texture realloaction for every fb effect
|
||||
// element every frame.
|
||||
// 2. The underlying blur visually expands. This is technically correct, since the
|
||||
// underlying contents shrink, but it's not what you visually expect: you expect the
|
||||
// blur to also shrink as the windows zoom out, to give the zooming out effect.
|
||||
//
|
||||
// Using size computed from geometry and scale solves both of those problems (even
|
||||
// though there's a bit of a cost in that zoomed-out elements still blur the entire
|
||||
// unzoomed texture size, and even though the blur ends up slightly wrong as there's two
|
||||
// layers of texture resampling, up and back down).
|
||||
//
|
||||
// Here we use src.size rather than geometry directly because src takes into account
|
||||
// cropping.
|
||||
let size = src
|
||||
.size
|
||||
.to_logical(1., Transform::Normal)
|
||||
.upscale(clamp_scale)
|
||||
.to_physical_precise_round(self.scale);
|
||||
let size = transform.transform_size(size);
|
||||
|
||||
let size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
|
||||
// Recreate framebuffer if needed.
|
||||
if inner
|
||||
.framebuffer
|
||||
.as_ref()
|
||||
.is_some_and(|fb| fb.size() != size)
|
||||
{
|
||||
inner.framebuffer = None;
|
||||
}
|
||||
let framebuffer = if let Some(fb) = &inner.framebuffer {
|
||||
fb
|
||||
} else {
|
||||
trace!("creating framebuffer texture sized {} × {}", size.w, size.h);
|
||||
let renderer = guard.as_mut();
|
||||
let texture = renderer.create_buffer(Fourcc::Abgr8888, size)?;
|
||||
inner.framebuffer.insert(texture)
|
||||
};
|
||||
|
||||
// Prepare blur textures.
|
||||
let mut blur = Option::zip(inner.blur.as_mut(), self.blur_options);
|
||||
if let Some((b, options)) = &mut blur {
|
||||
let renderer = guard.as_mut();
|
||||
if let Err(err) = b.prepare_textures(
|
||||
|fourcc, size| renderer.create_buffer(fourcc, size),
|
||||
framebuffer,
|
||||
*options,
|
||||
) {
|
||||
warn!("error preparing blur textures: {err:?}");
|
||||
blur = None;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't use renderer.with_context() as that will reset the GlesFrame binding that we
|
||||
// want to blit from.
|
||||
drop(guard);
|
||||
|
||||
// Blit the framebuffer contents.
|
||||
frame.with_context(|gl| unsafe {
|
||||
while gl.GetError() != ffi::NO_ERROR {}
|
||||
|
||||
let mut current_fbo = 0i32;
|
||||
gl.GetIntegerv(ffi::DRAW_FRAMEBUFFER_BINDING, &mut current_fbo as *mut _);
|
||||
|
||||
// BlitFramebuffer is affected by the scissor test, we don't want that.
|
||||
gl.Disable(ffi::SCISSOR_TEST);
|
||||
|
||||
let mut fbo = 0;
|
||||
gl.GenFramebuffers(1, &mut fbo as *mut _);
|
||||
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbo);
|
||||
|
||||
gl.FramebufferTexture2D(
|
||||
ffi::DRAW_FRAMEBUFFER,
|
||||
ffi::COLOR_ATTACHMENT0,
|
||||
ffi::TEXTURE_2D,
|
||||
framebuffer.tex_id(),
|
||||
0,
|
||||
);
|
||||
|
||||
gl.BlitFramebuffer(
|
||||
dst.loc.x,
|
||||
dst.loc.y,
|
||||
dst.loc.x + dst.size.w,
|
||||
dst.loc.y + dst.size.h,
|
||||
0,
|
||||
0,
|
||||
size.w,
|
||||
size.h,
|
||||
ffi::COLOR_BUFFER_BIT,
|
||||
ffi::LINEAR,
|
||||
);
|
||||
|
||||
// Restore state set by GlesFrame that we just modified.
|
||||
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, current_fbo as u32);
|
||||
gl.Enable(ffi::SCISSOR_TEST);
|
||||
|
||||
gl.DeleteFramebuffers(1, &mut fbo as *mut _);
|
||||
|
||||
if gl.GetError() != ffi::NO_ERROR {
|
||||
Err(GlesError::BlitError)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})??;
|
||||
|
||||
// If blur is off, use the unblurred texture.
|
||||
if self.blur_options.is_none() {
|
||||
inner.intermediate = Some(framebuffer.clone());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some((blur, options)) = blur {
|
||||
let mut guard = frame.renderer();
|
||||
let renderer = guard.as_mut();
|
||||
match blur.render(renderer, framebuffer, options) {
|
||||
Ok(blurred) => inner.intermediate = Some(blurred),
|
||||
Err(err) => {
|
||||
warn!("error rendering blur: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
_opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
let Some(cache) = cache else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(inner) = cache.get::<RefCell<Inner>>() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut inner = inner.borrow_mut();
|
||||
let inner = &mut *inner;
|
||||
|
||||
let Some(texture) = &inner.intermediate else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Clamp the same way as in capture_framebuffer().
|
||||
let output_rect = Rectangle::from_size(frame.output_size());
|
||||
let clamped_dst = match dst.intersection(output_rect) {
|
||||
Some(clamped) => clamped,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let clamp_offset = clamped_dst.loc - dst.loc;
|
||||
|
||||
// Filter damage by subregion, reusing the stored Vec to avoid allocation.
|
||||
let filtered = &mut inner.subregion_damage;
|
||||
filtered.clear();
|
||||
|
||||
if let Some(subregion) = &self.subregion {
|
||||
// Convert to subregion coordinates.
|
||||
let mut crop = src.to_logical(1., Transform::Normal, &src.size);
|
||||
crop.loc += self.geometry.loc;
|
||||
subregion.filter_damage(crop, dst, damage, filtered);
|
||||
} else {
|
||||
filtered.extend(damage.iter());
|
||||
};
|
||||
|
||||
// Adjust for clamped dst.
|
||||
if clamped_dst != dst {
|
||||
let r = Rectangle::new(clamp_offset, clamped_dst.size);
|
||||
filtered.retain_mut(|d| {
|
||||
if let Some(mut crop) = d.intersection(r) {
|
||||
crop.loc -= clamp_offset;
|
||||
*d = crop;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if filtered.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let damage = &filtered[..];
|
||||
|
||||
// Adjust src proportionally to the dst clamping.
|
||||
let src_loc = src.loc.to_logical(1., Transform::Normal, &src.size);
|
||||
let dst_to_src = src.size / dst.size.to_f64();
|
||||
let crop = Rectangle::new(
|
||||
src_loc + clamp_offset.to_f64().upscale(dst_to_src).to_logical(1.),
|
||||
clamped_dst.size.to_f64().upscale(dst_to_src).to_logical(1.),
|
||||
);
|
||||
|
||||
let program = Shaders::get_from_frame(frame).postprocess_and_clip.clone();
|
||||
let uniforms = program
|
||||
.is_some()
|
||||
.then(|| self.compute_uniforms(crop, frame.transformation()));
|
||||
let uniforms = uniforms.as_ref().map_or(&[][..], |x| &x[..]);
|
||||
|
||||
frame.render_texture_from_to(
|
||||
texture,
|
||||
Rectangle::from_size(texture.size().to_f64()),
|
||||
clamped_dst,
|
||||
damage,
|
||||
&[],
|
||||
// The intermediate texture has the same transform as the frame.
|
||||
frame.transformation().invert(),
|
||||
1.,
|
||||
program.as_ref(),
|
||||
uniforms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for FramebufferEffectElement {
|
||||
fn capture_framebuffer(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
cache: &UserDataMap,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::capture_framebuffer(&self, gles_frame, src, dst, cache)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn new(renderer: &mut GlesRenderer) -> Self {
|
||||
Inner {
|
||||
framebuffer: None,
|
||||
blur: Blur::new(renderer),
|
||||
intermediate: None,
|
||||
subregion_damage: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use smithay::backend::renderer::gles::{
|
||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
|
||||
};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::texture::TextureRenderElement;
|
||||
@@ -96,10 +97,19 @@ impl RenderElement<GlesRenderer> for GradientFadeTextureRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
let uniforms = vec![Uniform::new("cutoff", self.cutoff)];
|
||||
frame.override_default_tex_program(self.program.0.clone(), uniforms);
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.inner,
|
||||
frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
frame.clear_tex_program_override();
|
||||
Ok(())
|
||||
}
|
||||
@@ -119,9 +129,18 @@ impl<'render> RenderElement<TtyRenderer<'render>> for GradientFadeTextureRenderE
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+126
-35
@@ -1,27 +1,39 @@
|
||||
use std::ptr;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use anyhow::{ensure, Context as _};
|
||||
use niri_config::BlockOutFrom;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTarget, GlesTexture};
|
||||
use smithay::backend::renderer::element::{Element, Kind, RenderElement, RenderElementStates};
|
||||
use smithay::backend::renderer::gles::{
|
||||
GlesError, GlesMapping, GlesRenderer, GlesTarget, GlesTexture,
|
||||
};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::backend::renderer::{
|
||||
Bind, Color32F, ExportMem, Frame, Offscreen, Renderer, Texture as _,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::wayland::shm;
|
||||
use solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
|
||||
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use self::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::render_helpers::renderer::AsGlesRenderer;
|
||||
use crate::render_helpers::xray::Xray;
|
||||
|
||||
pub mod background_effect;
|
||||
pub mod blur;
|
||||
pub mod border;
|
||||
pub mod clipped_surface;
|
||||
pub mod damage;
|
||||
pub mod debug;
|
||||
pub mod effect_buffer;
|
||||
pub mod framebuffer_effect;
|
||||
pub mod gradient_fade_texture;
|
||||
pub mod memory;
|
||||
pub mod offscreen;
|
||||
@@ -37,12 +49,44 @@ pub mod snapshot;
|
||||
pub mod solid_color;
|
||||
pub mod surface;
|
||||
pub mod texture;
|
||||
pub mod xray;
|
||||
|
||||
/// A rendering context.
|
||||
///
|
||||
/// Bundles together things needed by most rendering code.
|
||||
pub struct RenderCtx<'a, R> {
|
||||
pub renderer: &'a mut R,
|
||||
pub target: RenderTarget,
|
||||
pub xray: Option<&'a Xray>,
|
||||
}
|
||||
|
||||
impl<'a, R> RenderCtx<'a, R> {
|
||||
/// Reborrows this context with a smaller lifetime.
|
||||
#[inline]
|
||||
pub fn r<'b>(&'b mut self) -> RenderCtx<'b, R> {
|
||||
RenderCtx {
|
||||
renderer: self.renderer,
|
||||
target: self.target,
|
||||
xray: self.xray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: AsGlesRenderer> RenderCtx<'a, R> {
|
||||
pub fn as_gles<'b>(&'b mut self) -> RenderCtx<'b, GlesRenderer> {
|
||||
RenderCtx {
|
||||
renderer: self.renderer.as_gles_renderer(),
|
||||
target: self.target,
|
||||
xray: self.xray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What we're rendering for.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RenderTarget {
|
||||
/// Rendering to display on screen.
|
||||
Output,
|
||||
Output = 0,
|
||||
/// Rendering for a screencast.
|
||||
Screencast,
|
||||
/// Rendering for any other screen capture.
|
||||
@@ -71,6 +115,8 @@ pub trait ToRenderElement {
|
||||
}
|
||||
|
||||
impl RenderTarget {
|
||||
pub const COUNT: usize = 3;
|
||||
|
||||
pub fn should_block_out(self, block_out_from: Option<BlockOutFrom>) -> bool {
|
||||
match block_out_from {
|
||||
None => false,
|
||||
@@ -126,6 +172,23 @@ pub fn encompassing_geo(
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn create_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
fourcc: Fourcc,
|
||||
) -> Result<GlesTexture, GlesError> {
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
renderer.create_buffer(fourcc, buffer_size)
|
||||
}
|
||||
|
||||
pub fn copy_framebuffer(
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &GlesTarget,
|
||||
fourcc: Fourcc,
|
||||
) -> Result<GlesMapping, GlesError> {
|
||||
renderer.copy_framebuffer(target, Rectangle::from_size(target.size()), fourcc)
|
||||
}
|
||||
|
||||
pub fn render_to_encompassing_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: Scale<f64>,
|
||||
@@ -154,11 +217,7 @@ pub fn render_to_texture(
|
||||
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
|
||||
let mut texture: GlesTexture = renderer
|
||||
.create_buffer(fourcc, buffer_size)
|
||||
.context("error creating texture")?;
|
||||
let mut texture = create_texture(renderer, size, fourcc).context("error creating texture")?;
|
||||
|
||||
let sync_point = {
|
||||
let mut target = renderer
|
||||
@@ -181,18 +240,15 @@ pub fn render_and_download(
|
||||
) -> anyhow::Result<GlesMapping> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let (mut texture, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
// FIXME: would be nice to avoid binding the second time here (after render_to_texture()), but
|
||||
// borrowing makes this inconvenient.
|
||||
let target = renderer
|
||||
let mut texture = create_texture(renderer, size, fourcc).context("error creating texture")?;
|
||||
let mut target = renderer
|
||||
.bind(&mut texture)
|
||||
.context("error binding texture")?;
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(buffer_size), fourcc)
|
||||
.context("error copying framebuffer")?;
|
||||
Ok(mapping)
|
||||
|
||||
let _sync = render_elements(renderer, &mut target, size, scale, transform, elements)
|
||||
.context("error rendering")?;
|
||||
|
||||
copy_framebuffer(renderer, &target, fourcc).context("error copying framebuffer")
|
||||
}
|
||||
|
||||
pub fn render_to_vec(
|
||||
@@ -215,33 +271,44 @@ pub fn render_to_vec(
|
||||
|
||||
pub fn render_to_dmabuf(
|
||||
renderer: &mut GlesRenderer,
|
||||
damage_tracker: &mut OutputDamageTracker,
|
||||
mut dmabuf: Dmabuf,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
states: RenderElementStates,
|
||||
) -> anyhow::Result<SyncPoint> {
|
||||
let _span = tracy_client::span!();
|
||||
let (size, _scale, _transform) = damage_tracker.mode().try_into().unwrap();
|
||||
ensure!(
|
||||
dmabuf.width() == size.w as u32 && dmabuf.height() == size.h as u32,
|
||||
"invalid buffer size"
|
||||
);
|
||||
let mut target = renderer
|
||||
.bind(&mut dmabuf)
|
||||
.context("error binding texture")?;
|
||||
render_elements(renderer, &mut target, size, scale, transform, elements)
|
||||
|
||||
let mut target = renderer.bind(&mut dmabuf).context("error binding dmabuf")?;
|
||||
let res = damage_tracker
|
||||
.render_output_with_states(
|
||||
renderer,
|
||||
&mut target,
|
||||
0,
|
||||
elements,
|
||||
Color32F::TRANSPARENT,
|
||||
states,
|
||||
)
|
||||
.context("error rendering to dmabuf")?;
|
||||
Ok(res.sync)
|
||||
}
|
||||
|
||||
pub fn render_to_shm(
|
||||
renderer: &mut GlesRenderer,
|
||||
damage_tracker: &mut OutputDamageTracker,
|
||||
buffer: &WlBuffer,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
states: RenderElementStates,
|
||||
) -> anyhow::Result<()> {
|
||||
let _span = tracy_client::span!();
|
||||
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
|
||||
let (size, _scale, _transform) = damage_tracker.mode().try_into().unwrap();
|
||||
let fourcc = Fourcc::Xrgb8888;
|
||||
|
||||
ensure!(
|
||||
// The buffer prefers pixels in little endian ...
|
||||
buffer_data.format == wl_shm::Format::Xrgb8888
|
||||
@@ -251,9 +318,26 @@ pub fn render_to_shm(
|
||||
&& shm_len == buffer_data.stride as usize * buffer_data.height as usize,
|
||||
"invalid buffer format or size"
|
||||
);
|
||||
let mapping =
|
||||
render_and_download(renderer, size, scale, transform, Fourcc::Xrgb8888, elements)?;
|
||||
|
||||
let mut texture =
|
||||
create_texture(renderer, size, fourcc).context("error creating texture")?;
|
||||
let mut target = renderer
|
||||
.bind(&mut texture)
|
||||
.context("error binding texture")?;
|
||||
|
||||
let _res = damage_tracker
|
||||
.render_output_with_states(
|
||||
renderer,
|
||||
&mut target,
|
||||
0,
|
||||
elements,
|
||||
Color32F::TRANSPARENT,
|
||||
states,
|
||||
)
|
||||
.context("error rendering")?;
|
||||
|
||||
let mapping =
|
||||
copy_framebuffer(renderer, &target, fourcc).context("error copying framebuffer")?;
|
||||
let bytes = renderer
|
||||
.map_texture(&mapping)
|
||||
.context("error mapping texture")?;
|
||||
@@ -306,8 +390,15 @@ fn render_elements(
|
||||
|
||||
if let Some(mut damage) = output_rect.intersection(dst) {
|
||||
damage.loc -= dst.loc;
|
||||
|
||||
let cache = UserDataMap::new();
|
||||
if element.is_framebuffer_effect() {
|
||||
element
|
||||
.capture_framebuffer(&mut frame, src, dst, &cache)
|
||||
.context("error in capture_framebuffer()")?;
|
||||
}
|
||||
element
|
||||
.draw(&mut frame, src, dst, &[damage], &[])
|
||||
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
|
||||
.context("error drawing element")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use smithay::backend::renderer::utils::{
|
||||
use smithay::backend::renderer::{
|
||||
Bind as _, Color32F, ContextId, Frame as _, Offscreen as _, Renderer, Texture as _,
|
||||
};
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::encompassing_geo;
|
||||
@@ -81,7 +82,12 @@ impl OffscreenBuffer {
|
||||
RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative)
|
||||
}));
|
||||
|
||||
let src_size = geo.size;
|
||||
// Guard against empty elements producing a zero size.
|
||||
let mut src_size = geo.size;
|
||||
if src_size.w == 0 || src_size.h == 0 {
|
||||
src_size = Size::new(1, 1);
|
||||
}
|
||||
|
||||
let src_size = src_size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
let offset = geo.loc.to_f64().to_logical(scale);
|
||||
|
||||
@@ -157,13 +163,10 @@ impl OffscreenBuffer {
|
||||
|
||||
let res = {
|
||||
let mut target = renderer.bind(&mut inner.texture)?;
|
||||
inner.damage.render_output(
|
||||
renderer,
|
||||
&mut target,
|
||||
1,
|
||||
&elements,
|
||||
Color32F::TRANSPARENT,
|
||||
)?
|
||||
inner
|
||||
.damage
|
||||
.render_output(renderer, &mut target, 1, &elements, Color32F::TRANSPARENT)
|
||||
.context("error rendering")?
|
||||
};
|
||||
|
||||
// Add the resulting damage to the outer tracker.
|
||||
@@ -304,6 +307,7 @@ impl RenderElement<GlesRenderer> for OffscreenRenderElement {
|
||||
dest: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
_cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
if frame.context_id() != self.renderer_context_id {
|
||||
warn!("trying to render texture from different renderer");
|
||||
@@ -338,9 +342,18 @@ impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::renderer::AsGlesFrame;
|
||||
@@ -61,9 +62,18 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.0,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -82,9 +92,18 @@ impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderEle
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.0,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -93,11 +93,31 @@ macro_rules! niri_render_elements {
|
||||
$($name::$variant(elem) => elem.kind()),+
|
||||
}
|
||||
}
|
||||
|
||||
fn is_framebuffer_effect(&self) -> bool {
|
||||
match self {
|
||||
$($name::$variant(elem) => elem.is_framebuffer_effect()),+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
|
||||
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
|
||||
{
|
||||
fn capture_framebuffer(
|
||||
&self,
|
||||
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
cache: &smithay::utils::user_data::UserDataMap,
|
||||
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::capture_framebuffer(elem, frame, src, dst, cache)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>,
|
||||
@@ -105,10 +125,11 @@ macro_rules! niri_render_elements {
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
opaque_regions: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
cache: Option<&smithay::utils::user_data::UserDataMap>,
|
||||
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage, opaque_regions)
|
||||
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage, opaque_regions, cache)
|
||||
})+
|
||||
}
|
||||
}
|
||||
@@ -123,6 +144,20 @@ macro_rules! niri_render_elements {
|
||||
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
|
||||
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
|
||||
{
|
||||
fn capture_framebuffer(
|
||||
&self,
|
||||
frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
cache: &smithay::utils::user_data::UserDataMap,
|
||||
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::capture_framebuffer(elem, frame, src, dst, cache)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>,
|
||||
@@ -130,10 +165,11 @@ macro_rules! niri_render_elements {
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
opaque_regions: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
cache: Option<&smithay::utils::user_data::UserDataMap>,
|
||||
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
|
||||
match self {
|
||||
$($name::$variant(elem) => {
|
||||
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage, opaque_regions)
|
||||
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage, opaque_regions, cache)
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesT
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::backend::renderer::Texture as _;
|
||||
use smithay::gpu_span_location;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::{AsGlesFrame, NiriRenderer};
|
||||
@@ -171,10 +172,19 @@ impl RenderElement<GlesRenderer> for ResizeRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
let _span = tracy_client::span!("ResizeRenderElement::draw");
|
||||
frame.with_gpu_span(gpu_span_location!("ResizeRenderElement::draw"), |frame| {
|
||||
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage, opaque_regions)
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.0,
|
||||
frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
cache,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -191,9 +201,10 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let frame = frame.as_gles_frame();
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions, cache)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use smithay::backend::renderer::gles::{
|
||||
};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
|
||||
use smithay::backend::renderer::DebugFlags;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::renderer::AsGlesFrame;
|
||||
@@ -293,6 +294,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
dest: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
_opaque_regions: &[Rectangle<i32, Physical>],
|
||||
_cache: Option<&UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
let _span = tracy_client::span!("ShaderRenderElement::draw");
|
||||
|
||||
@@ -527,10 +529,11 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ShaderRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
cache: Option<&UserDataMap>,
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let frame = frame.as_gles_frame();
|
||||
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
|
||||
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions, cache)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#version 100
|
||||
|
||||
attribute vec2 vert;
|
||||
varying vec2 v_coords;
|
||||
|
||||
void main() {
|
||||
v_coords = vert;
|
||||
// vert goes from 0 to 1; position must be from -1 to 1.
|
||||
vec2 position = vert * 2.0 - 1.0;
|
||||
gl_Position = vec4(position, 1.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#version 100
|
||||
|
||||
precision highp float;
|
||||
|
||||
varying vec2 v_coords;
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform vec2 half_pixel;
|
||||
uniform float offset;
|
||||
|
||||
void main() {
|
||||
vec2 o = half_pixel * offset;
|
||||
|
||||
vec4 sum = texture2D(tex, v_coords) * 4.0;
|
||||
sum += texture2D(tex, v_coords + vec2(-o.x, -o.y));
|
||||
sum += texture2D(tex, v_coords + vec2( o.x, -o.y));
|
||||
sum += texture2D(tex, v_coords + vec2(-o.x, o.y));
|
||||
sum += texture2D(tex, v_coords + vec2( o.x, o.y));
|
||||
|
||||
gl_FragColor = sum / 8.0;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#version 100
|
||||
|
||||
precision highp float;
|
||||
|
||||
varying vec2 v_coords;
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform vec2 half_pixel;
|
||||
uniform float offset;
|
||||
|
||||
void main() {
|
||||
vec2 o = half_pixel * offset;
|
||||
|
||||
vec4 sum = vec4(0.0);
|
||||
|
||||
// Four edge centers
|
||||
sum += texture2D(tex, v_coords + vec2(-o.x * 2.0, 0.0));
|
||||
sum += texture2D(tex, v_coords + vec2( o.x * 2.0, 0.0));
|
||||
sum += texture2D(tex, v_coords + vec2(0.0, -o.y * 2.0));
|
||||
sum += texture2D(tex, v_coords + vec2(0.0, o.y * 2.0));
|
||||
|
||||
// Four diagonal corners
|
||||
sum += texture2D(tex, v_coords + vec2(-o.x, o.y)) * 2.0;
|
||||
sum += texture2D(tex, v_coords + vec2( o.x, o.y)) * 2.0;
|
||||
sum += texture2D(tex, v_coords + vec2(-o.x, -o.y)) * 2.0;
|
||||
sum += texture2D(tex, v_coords + vec2( o.x, -o.y)) * 2.0;
|
||||
|
||||
gl_FragColor = sum / 12.0;
|
||||
}
|
||||
@@ -208,35 +208,12 @@ vec4 gradient_color(vec2 coords) {
|
||||
return color_mix(color_from, color_to, frac);
|
||||
}
|
||||
|
||||
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
|
||||
vec2 center;
|
||||
float radius;
|
||||
|
||||
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
|
||||
radius = corner_radius.x;
|
||||
center = vec2(radius, radius);
|
||||
} else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) {
|
||||
radius = corner_radius.y;
|
||||
center = vec2(size.x - radius, radius);
|
||||
} else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) {
|
||||
radius = corner_radius.z;
|
||||
center = vec2(size.x - radius, size.y - radius);
|
||||
} else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) {
|
||||
radius = corner_radius.w;
|
||||
center = vec2(radius, size.y - radius);
|
||||
} else {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float dist = distance(coords, center);
|
||||
float half_px = 0.5 / niri_scale;
|
||||
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
|
||||
}
|
||||
float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius);
|
||||
|
||||
void main() {
|
||||
vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0);
|
||||
vec4 color = gradient_color(coords_geo.xy);
|
||||
color = color * rounding_alpha(coords_geo.xy, geo_size, outer_radius);
|
||||
color = color * niri_rounding_alpha(coords_geo.xy, geo_size, outer_radius);
|
||||
|
||||
if (border_width > 0.0) {
|
||||
coords_geo -= vec3(border_width);
|
||||
@@ -245,7 +222,7 @@ void main() {
|
||||
&& 0.0 <= coords_geo.y && coords_geo.y <= inner_geo_size.y)
|
||||
{
|
||||
vec4 inner_radius = max(outer_radius - vec4(border_width), 0.0);
|
||||
color = color * (1.0 - rounding_alpha(coords_geo.xy, inner_geo_size, inner_radius));
|
||||
color = color * (1.0 - niri_rounding_alpha(coords_geo.xy, inner_geo_size, inner_radius));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,30 +26,8 @@ uniform vec2 geo_size;
|
||||
uniform vec4 corner_radius;
|
||||
uniform mat3 input_to_geo;
|
||||
|
||||
float rounding_alpha(vec2 coords, vec2 size) {
|
||||
vec2 center;
|
||||
float radius;
|
||||
|
||||
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
|
||||
radius = corner_radius.x;
|
||||
center = vec2(radius, radius);
|
||||
} else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) {
|
||||
radius = corner_radius.y;
|
||||
center = vec2(size.x - radius, radius);
|
||||
} else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) {
|
||||
radius = corner_radius.z;
|
||||
center = vec2(size.x - radius, size.y - radius);
|
||||
} else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) {
|
||||
radius = corner_radius.w;
|
||||
center = vec2(radius, size.y - radius);
|
||||
} else {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float dist = distance(coords, center);
|
||||
float half_px = 0.5 / niri_scale;
|
||||
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
|
||||
}
|
||||
float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius);
|
||||
vec4 postprocess(vec4 color);
|
||||
|
||||
void main() {
|
||||
vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0);
|
||||
@@ -60,12 +38,14 @@ void main() {
|
||||
color = vec4(color.rgb, 1.0);
|
||||
#endif
|
||||
|
||||
color = postprocess(color);
|
||||
|
||||
if (coords_geo.x < 0.0 || 1.0 < coords_geo.x || coords_geo.y < 0.0 || 1.0 < coords_geo.y) {
|
||||
// Clip outside geometry.
|
||||
color = vec4(0.0);
|
||||
} else {
|
||||
// Apply corner rounding inside geometry.
|
||||
color = color * rounding_alpha(coords_geo.xy * geo_size, geo_size);
|
||||
color = color * niri_rounding_alpha(coords_geo.xy * geo_size, geo_size, corner_radius);
|
||||
}
|
||||
|
||||
// Apply final alpha and tint.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user