mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e20a369491 | |||
| 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 | |||
| f30db163b5 | |||
| a78f07cd58 | |||
| 765a241c5a | |||
| a00b271a15 | |||
| e1015ac92f | |||
| a34ed51586 | |||
| 5ddcf195dd | |||
| e11abe554f | |||
| 9261fd6342 | |||
| fb2f66f361 | |||
| e2e15b7a18 | |||
| 0a416eedda | |||
| d7184a04b9 | |||
| bdf394260a | |||
| 74d14be01f | |||
| 3ccb06f564 | |||
| d9e755d575 | |||
| 87e2dd0361 | |||
| dd93c39ed0 | |||
| 849788bb28 | |||
| 9015ff8e36 | |||
| e546b339a3 | |||
| b39edf405a | |||
| b98f4906da | |||
| e82830c68c | |||
| 238caaf8da | |||
| 9c79108afa | |||
| 2571242887 | |||
| 6f92b3296a | |||
| 570ea119ba | |||
| df4614e62c | |||
| 3672e79369 | |||
| 2d16abdaae | |||
| ff081acddc | |||
| afe27a143b | |||
| fd2916eb72 | |||
| e9d888cd52 | |||
| 05599ce2c4 | |||
| 0fb6c5706b | |||
| 79aaa4c6c0 | |||
| 7e559dc468 | |||
| 45fc763281 | |||
| 39d3cd2415 | |||
| 19b1074a8b | |||
| 539a5a8030 | |||
| 53b7477d20 | |||
| c34f7b18ec | |||
| a6baef7b68 | |||
| 10df9f4717 | |||
| 9f8eadc5bc | |||
| a496307daf | |||
| bc7bb51b6f | |||
| b7eb8a635b | |||
| d060b06667 | |||
| 54c2e2ab47 | |||
| df3f3979e9 | |||
| 6215b5f0b1 | |||
| 3bfa4a71ff | |||
| 3158f5a9c0 | |||
| d8250fa876 | |||
| cf0b4bc0ca | |||
| 1ab1737653 | |||
| b5640d5293 | |||
| 860a08cce6 | |||
| 2a9d0e495a | |||
| 7f132ecf95 | |||
| 1a63089d67 | |||
| 88dc6e22d0 | |||
| ce8171bed3 | |||
| 6edd29170f | |||
| 9d62b94688 | |||
| 4d295418ce | |||
| f01d48bc51 | |||
| 31ca509160 | |||
| 396097c3ab | |||
| ad62c8e487 | |||
| 9e73beb165 | |||
| 4fca614510 | |||
| 19e55a2df0 | |||
| 6472209b45 | |||
| d9ceff7c70 | |||
| 813c5ee05f | |||
| 47e217c00e | |||
| 9b52465e42 | |||
| 7d60231e35 | |||
| 7a237e519c | |||
| c4462d0c7f | |||
| f85cb5c5f9 | |||
| 7ca46b44b2 | |||
| f913219f94 | |||
| 80469abc20 | |||
| 890935d2ba | |||
| d2fa1f54d4 | |||
| 2641356d41 | |||
| 7c0898570c | |||
| d1fc1ab731 | |||
| d9a9e6ddc4 | |||
| 0cb20b55b8 | |||
| 3d2d7b95d9 | |||
| c22d8358c2 | |||
| 4d058e6111 | |||
| 83a733e085 | |||
| ba29735fbb | |||
| 6fc092cc4f | |||
| f874b2fce5 | |||
| 311ca6b5da |
@@ -10,6 +10,13 @@ assignees: ''
|
||||
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||
|
||||
<!-- Attaching your full niri config can help diagnose the problem. -->
|
||||
<details><summary>Config</summary>
|
||||
|
||||
```kdl
|
||||
insert config here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<!--
|
||||
If you have a problem with a specific app, please verify that it is running on Wayland, rather than X11. An easy way is to run xeyes and mouse over the app: xeyes will be able to "see" only X11 windows.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,10 +13,12 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "Andrew-Chen-Wang/github-wiki-action"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
+20
-27
@@ -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
|
||||
|
||||
@@ -181,7 +181,7 @@ jobs:
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
- uses: dtolnay/rust-toolchain@1.85.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -246,38 +246,35 @@ jobs:
|
||||
- run: cargo build --all
|
||||
|
||||
freebsd:
|
||||
if: false # Waiting for a new version of the pipewire-rs patch.
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
CARGO_HOME: /home/runner/work/niri/niri/cargo-home
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
# Required for the rust-cache action to work.
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Remove man-db triggers to speed up Ubuntu upgrade by a minute or two during vmactions/freebsd-vm action run.
|
||||
- run: |
|
||||
sudo rm /var/lib/dpkg/info/man-db.*
|
||||
|
||||
- name: Build
|
||||
uses: vmactions/freebsd-vm@966989c456d41351f095a421f60e71342d3bce41 # v1.2.1
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
release: "15.0"
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg update -f
|
||||
pkg install -y ${{ env.DEPS_PKG }}
|
||||
run: |
|
||||
curl -o patch-pipewire_init 'https://cgit.freebsd.org/ports/plain/x11-wm/niri/files/patch-pipewire_init?id=f3f7e555b06d9a87d63c047ce3e82e936a11f2fe'
|
||||
curl -o patch-pipewire_init 'https://cgit.freebsd.org/ports/plain/x11-wm/niri/files/patch-pipewire_init?id=cadf6784d264cf780b6e0ad59bd15b831d36cf80'
|
||||
|
||||
export CARGO_HOME="$PWD/cargo-home"
|
||||
|
||||
cargo fetch
|
||||
|
||||
( cd $CARGO_HOME/git/checkouts/pipewire-rs-*/*/; patch -p2 < $CARGO_HOME/../patch-pipewire_init; )
|
||||
( cd $CARGO_HOME/registry/src/index.crates.io-*/; patch -p1 < $CARGO_HOME/../patch-pipewire_init; )
|
||||
|
||||
cargo build \
|
||||
--offline \
|
||||
@@ -292,16 +289,12 @@ jobs:
|
||||
dotnet: false
|
||||
large-packages: false
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check flake inputs
|
||||
uses: DeterminateSystems/flake-checker-action@v4
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
uses: cachix/install-nix-action@v31
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix flake check
|
||||
@@ -315,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
|
||||
@@ -332,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
|
||||
+20
-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,12 +84,30 @@ 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.
|
||||
- When opening a pull request, ensure "Allow edits from maintainers" is enabled, so I can make final tweaks before merging.
|
||||
|
||||
### How to get your pull request reviewed more quickly
|
||||
|
||||
- Make it small and self-contained. Avoid mixing several unrelated changes in one PR.
|
||||
- Split the PR into small and self-contained commits. This makes it much easier to review.
|
||||
- Discuss new features, options, or behavior changes beforehand; make sure there's consensus about the design.
|
||||
- When creating the pull request, clearly write what it does, what problem it solves, how to test it.
|
||||
- Follow the rest of the advice from this document.
|
||||
|
||||
## AI contributions
|
||||
|
||||
If you use LLMs for your contribution (issue, comment, pull request), then it is *your job* to check and clean up its output, just like with any other tool.
|
||||
*You* have to spend the time doing this.
|
||||
Particularly:
|
||||
|
||||
- If I can tell that a pull request is mostly LLM-generated, then very likely this pull request will take *significantly more time and effort* than usual to review and finish. This is based on my prior review experience. Therefore, I'm not interested in such pull requests—there's always plenty of human-written ones which take priority.
|
||||
- When using an LLM to prepare an issue, the text usually has a lot of unnecessary wording and irrelevant details. Anyone looking at such an issue will quickly lose interest in reading through it (myself certainly). Clean up the text and keep only those details that actually matter.
|
||||
- When using an LLM to comment on an issue, *you* have to verify that the comment makes sense, contributes something useful, and doesn't have unnecessary repetition.
|
||||
|
||||
|
||||
[cosmic-comp]: https://github.com/pop-os/cosmic-comp
|
||||
[anvil]: https://github.com/Smithay/smithay/tree/master/anvil
|
||||
|
||||
Generated
+921
-1239
File diff suppressed because it is too large
Load Diff
+51
-47
@@ -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"
|
||||
rust-version = "1.80.1"
|
||||
repository = "https://github.com/niri-wm/niri"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bitflags = "2.9.4"
|
||||
clap = { version = "4.5.48", features = ["derive"] }
|
||||
insta = "1.43.2"
|
||||
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.145"
|
||||
tracing = { version = "0.1.41", 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"] }
|
||||
tracy-client = { version = "0.18.3", default-features = false }
|
||||
serde_json = "1.0.149"
|
||||
tracing = { version = "0.1.44", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
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,51 +51,53 @@ readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
accesskit = { version = "0.21.0", optional = true }
|
||||
accesskit_unix = { version = "0.17.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.23.2", 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.58"
|
||||
clap_complete_nushell = "4.5.8"
|
||||
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.8"
|
||||
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.176"
|
||||
libc = "0.2.185"
|
||||
libdisplay-info = "0.3.0"
|
||||
log = { version = "0.4.28", 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"
|
||||
pango = { version = "0.20.12", features = ["v1_44"] }
|
||||
pangocairo = "0.20.10"
|
||||
log = { version = "0.4.29", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
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"
|
||||
portable-atomic = { version = "1.11.1", default-features = false, features = ["float"] }
|
||||
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
|
||||
url = { version = "2.5.7", optional = true }
|
||||
wayland-backend = "0.3.11"
|
||||
wayland-scanner = "0.31.7"
|
||||
wayland-backend = "0.3.15"
|
||||
wayland-scanner = "0.31.10"
|
||||
wayland-server = "0.31.13"
|
||||
xcursor = "0.3.10"
|
||||
zbus = { version = "5.11.0", optional = true }
|
||||
zbus = { version = "5.13.2", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -121,22 +121,25 @@ features = [
|
||||
approx = "0.5.1"
|
||||
calloop-wayland-source = "0.4.1"
|
||||
insta.workspace = true
|
||||
proptest = "1.8.0"
|
||||
proptest-derive = { version = "0.6.0", features = ["boxed_union"] }
|
||||
rayon = "1.11.0"
|
||||
wayland-client = "0.31.11"
|
||||
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.33"
|
||||
|
||||
[features]
|
||||
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, accessibility tree, power button handling).
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:url", "dep:accesskit", "dep:accesskit_unix"]
|
||||
dbus = ["dep:zbus", "dep:async-io", "dep:accesskit", "dep:accesskit_unix"]
|
||||
# Enables systemd integration (global environment, apps in transient scopes).
|
||||
systemd = ["dbus"]
|
||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||
xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
# Enables the Tracy profiler instrumentation.
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default", "smithay/tracy_gpu_profiling"]
|
||||
# Enables the on-demand Tracy profiler instrumentation.
|
||||
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
|
||||
# Enables Tracy allocation profiling.
|
||||
@@ -146,6 +149,7 @@ dinit = []
|
||||
|
||||
[lints.clippy]
|
||||
new_without_default = "allow"
|
||||
collapsible_match = "allow"
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
@@ -161,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,12 +2,12 @@
|
||||
<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>
|
||||
|
||||

|
||||
@@ -29,23 +29,23 @@ 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
|
||||
- [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,7 +55,7 @@ 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.
|
||||
Follow the instructions on the [Getting Started](https://niri-wm.github.io/niri/Getting-Started.html) page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
Also check out [awesome-niri], a list of niri-related links and projects.
|
||||
|
||||
@@ -72,7 +72,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 +93,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
|
||||
|
||||
@@ -121,7 +121,7 @@ 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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
fn main() {
|
||||
println!("cargo:rustc-check-cfg=cfg(have_libinput_plugin_system)");
|
||||
if pkg_config::Config::new()
|
||||
.atleast_version("1.30.0")
|
||||
.probe("libinput")
|
||||
.is_ok()
|
||||
{
|
||||
println!("cargo:rustc-cfg=have_libinput_plugin_system")
|
||||
}
|
||||
}
|
||||
@@ -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>"
|
||||
|
||||
+3
-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
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ dependencies = [
|
||||
]
|
||||
|
||||
# for KDL highlighting support
|
||||
# TODO: use the official pygments package once https://github.com/pygments/pygments/pull/2936 is merged
|
||||
# FIXME: use the official pygments package once https://github.com/pygments/pygments/pull/2936 is merged
|
||||
[tool.uv.sources]
|
||||
pygments = { git = "https://github.com/chinatsu/pygments", rev = "0f0b0d4da2839e1285881389155bb4605a0a6dc4" }
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
Electron-based applications can run directly on Wayland, but it's not the default.
|
||||
|
||||
For Electron > 28, you can set an environment variable:
|
||||
For Electron ≥ 39, you can use the command-line flag if the app does not default to Wayland:
|
||||
```
|
||||
--ozone-platform=wayland
|
||||
```
|
||||
|
||||
For Electron < 39, you can set an environment variable:
|
||||
```kdl
|
||||
environment {
|
||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||
}
|
||||
```
|
||||
|
||||
For previous versions, you need to pass command-line flags to the target application:
|
||||
For Electron ≤ 28, you need to pass command-line flags to the target application:
|
||||
```
|
||||
--enable-features=UseOzonePlatform --ozone-platform-hint=auto
|
||||
```
|
||||
@@ -22,6 +27,12 @@ If you're having issues with some VSCode hotkeys, try starting `Xwayland` and se
|
||||
That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance.
|
||||
Apparently, VSCode currently unconditionally queries the X server for a keymap.
|
||||
|
||||
### JetBrains IDEs
|
||||
|
||||
JetBrains IDEs can run directly on Wayland, but it's not the default.
|
||||
|
||||
For JetBrainsRuntime > 17, you can set the flag `-Dawt.toolkit.name=WLToolkit` inside of `help -> edit custom vm options -> add`.
|
||||
|
||||
### WezTerm
|
||||
|
||||
> [!NOTE]
|
||||
@@ -63,6 +74,9 @@ environment {
|
||||
}
|
||||
```
|
||||
|
||||
Note that the niri environment config does not propagate to apps and shells started by systemd, for example to DankMaterialShell and its application launcher.
|
||||
You can set the variable in your login shell config (i.e. `~/.bash_profile`) instead, though keep in mind that then it will be set for all compositors, not just niri.
|
||||
|
||||
### Fullscreen games
|
||||
|
||||
Some video games, both Linux-native and on Wine, have various issues when using non-stacking desktop environments.
|
||||
|
||||
@@ -17,6 +17,7 @@ debug {
|
||||
disable-cursor-plane
|
||||
disable-direct-scanout
|
||||
restrict-primary-scanout-to-matching-format
|
||||
force-disable-connectors-on-resume
|
||||
render-drm-device "/dev/dri/renderD129"
|
||||
ignore-drm-device "/dev/dri/renderD128"
|
||||
ignore-drm-device "/dev/dri/renderD130"
|
||||
@@ -104,6 +105,21 @@ 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.
|
||||
|
||||
If niri rendering is corrupted, or monitors don't light up after a TTY switch, you can try this flag.
|
||||
|
||||
```kdl
|
||||
debug {
|
||||
force-disable-connectors-on-resume
|
||||
}
|
||||
```
|
||||
|
||||
### `render-drm-device`
|
||||
|
||||
Override the DRM device that niri will use for all rendering.
|
||||
|
||||
@@ -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
|
||||
@@ -114,6 +120,30 @@ window-rule {
|
||||
}
|
||||
```
|
||||
|
||||
### Optional includes
|
||||
|
||||
<sup>Since: 26.04</sup>
|
||||
|
||||
By default, including a nonexistent file will cause an error.
|
||||
You can allow nonexistent includes by setting `optional=true`:
|
||||
|
||||
```kdl,must-fail
|
||||
// Won't fail if this file doesn't exist.
|
||||
include optional=true "optional-config.kdl"
|
||||
|
||||
// Regular include, will fail if the file doesn't exist.
|
||||
include "required-config.kdl"
|
||||
```
|
||||
|
||||
When an optional include file is missing, niri will emit a warning in the logs on every config reload.
|
||||
This reminds you that the file is missing while still loading the config successfully.
|
||||
|
||||
The optional file is still watched for changes, so if you create it later, the config will automatically reload and apply the new settings.
|
||||
|
||||
Note that `optional` only affects whether a missing file causes an error.
|
||||
If the file exists but contains invalid syntax or other errors, those errors will still cause a parsing failure.
|
||||
|
||||
|
||||
### Merging
|
||||
|
||||
Most config sections are merged between includes, meaning that you can set only a few properties, and only those properties will change.
|
||||
|
||||
@@ -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,6 +382,17 @@ binds {
|
||||
}
|
||||
```
|
||||
|
||||
<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
|
||||
binds {
|
||||
// The pointer will be visible on the screenshot
|
||||
// if it's on top of the window.
|
||||
Alt+Print { screenshot-window show-pointer=true; }
|
||||
}
|
||||
```
|
||||
|
||||
#### `toggle-keyboard-shortcuts-inhibit`
|
||||
|
||||
<sup>Since: 25.02</sup>
|
||||
|
||||
@@ -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`
|
||||
@@ -141,6 +149,13 @@ environment {
|
||||
}
|
||||
```
|
||||
|
||||
Note that these variables do not propagate to the systemd global environment, so tools and applications started by systemd do not see them.
|
||||
In particular, if you start a desktop shell like DankMaterialShell through systemd, then use its built-in application launcher, the apps won't see these environment variables.
|
||||
|
||||
If you want all processes to see the environment variables, you can set them in your login shell config instead (i.e. `~/.bash_profile`).
|
||||
The `niri-session` shell script runs through the login shell and imports all environment variables to systemd before starting niri.
|
||||
Keep in mind that all compositors will see variables set in the login shell, not just niri.
|
||||
|
||||
### `cursor`
|
||||
|
||||
Change the theme and size of the cursor as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables.
|
||||
@@ -313,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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,6 +39,9 @@ switch-events {
|
||||
These events trigger when a convertible laptop goes into or out of tablet mode.
|
||||
In tablet mode, the keyboard and mouse are usually inaccessible, so you can use these events to activate the on-screen keyboard.
|
||||
|
||||
> [!NOTE]
|
||||
> The commands below are just examples, you will need to provide your own on-screen keyboard, such as [sysboard](https://github.com/System64fumo/sysboard) or [wvkbd](https://github.com/jjsullivan5196/wvkbd).
|
||||
|
||||
```kdl
|
||||
switch-events {
|
||||
tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+20
-8
@@ -40,12 +40,26 @@ 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).
|
||||
Check [the Xwayland wiki page](./Xwayland.md) for instructions.
|
||||
|
||||
Keep in mind that you can run many Electron apps such as VSCode natively on Wayland by passing the right flags, e.g. `code --ozone-platform-hint=auto`
|
||||
Keep in mind that you can run many Electron apps such as VSCode or Discord natively on Wayland by passing the right flags, as described [here](./Application-Issues.md#electron-applications).
|
||||
|
||||
### Why doesn't niri integrate Xwayland like other compositors?
|
||||
|
||||
@@ -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 wl-clipboard cliphist 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
|
||||
```
|
||||
|
||||
@@ -29,6 +28,8 @@ Or, if not using a display manager, run `niri-session` on a TTY.
|
||||
The default niri config will run Waybar, so you might get two bars on screen.
|
||||
To fix this, stop Waybar with `pkill waybar` command, then open `~/.config/niri/config.kdl` and delete the `spawn-at-startup "waybar"` line.
|
||||
|
||||
Check the DankMaterialShell's [compositor setup page](https://danklinux.com/docs/dankmaterialshell/compositors#niri-configuration) to learn how to configure DMS-specific binds and other niri integrations.
|
||||
|
||||
## Slower and more considered start
|
||||
|
||||
The easiest way to get niri is to install one of the distribution packages.
|
||||
@@ -145,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% |
|
||||
@@ -223,7 +221,7 @@ This defaults to `/usr/bin/niri`.
|
||||
| `resources/niri.service` (systemd) | `/etc/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` (systemd) | `/etc/systemd/user/` |
|
||||
| `resources/dinit/niri` (dinit) | `/etc/dinit.d/user/` |
|
||||
| `resources/dinit/niri-shutdown` (dinit) | `/etc/dinit.d/user/` |
|
||||
| `resources/dinit/niri.target` (dinit) | `/etc/dinit.d/user/` |
|
||||
|
||||
[Alacritty]: https://github.com/alacritty/alacritty
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
+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
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ Note that if you're using the provided `resources/niri-portals.conf`, you also n
|
||||
|
||||
If you do not want to install `nautilus` (say you use `nemo` instead), you can set `org.freedesktop.impl.portal.FileChooser=gtk;` in `niri-portals.conf` to use the GTK portal for file chooser dialogues.
|
||||
|
||||
> [!WARNING]
|
||||
> Do not set the `GDK_BACKEND` environment variable globally as this will break the screencast portal.
|
||||
|
||||
### Authentication Agent
|
||||
|
||||
Required when apps need to ask for root permissions. Something like `plasma-polkit-agent` works fine. Start it [with systemd](./Example-systemd-Setup.md) or with [`spawn-at-startup`](./Configuration:-Miscellaneous.md#spawn-at-startup).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Splitting the niri config file into multiple files, or includes, are not supported yet.
|
||||
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.
|
||||
|
||||
@@ -24,12 +24,31 @@ To do that, put files into the correct directories according to this table.
|
||||
| `resources/niri.service` (systemd) | `/usr/lib/systemd/user/` |
|
||||
| `resources/niri-shutdown.target` (systemd) | `/usr/lib/systemd/user/` |
|
||||
| `resources/dinit/niri` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
| `resources/dinit/niri-shutdown` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
| `resources/dinit/niri.target` (dinit) | `/usr/lib/dinit.d/user/` |
|
||||
|
||||
Doing this will make niri appear in GDM and other display managers.
|
||||
|
||||
See the [Integrating niri](./Integrating-niri.md) page for further information on distribution integration.
|
||||
|
||||
### Recommended dependencies
|
||||
|
||||
First of all, make sure niri depends on `libwayland-server`.
|
||||
This library is currently loaded dynamically, so it's not picked up as a dependency at niri build time.
|
||||
|
||||
Then, the following dependencies are optional, but strongly recommended.
|
||||
Set them as automatically-installed optional dependencies, if possible.
|
||||
|
||||
- `xwayland-satellite`: required to run X11 applications (Steam, Discord, etc.).
|
||||
- `xdg-desktop-portal-gnome`: required for screencasting.
|
||||
- `xdg-desktop-portal-gtk`: configured as the fallback portal in `niri-portals.conf`.
|
||||
(This is in general the standard fallback portal that you want installed.)
|
||||
- `gnome-keyring`: configured as the Secret portal provider in `niri-portals.conf`.
|
||||
- Your distro's GPU driver package, such as `mesa-dri-drivers` and `mesa-libEGL`.
|
||||
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/niri-wm/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
|
||||
|
||||
### Running tests
|
||||
|
||||
A bulk of our tests spawn niri compositor instances and test Wayland clients.
|
||||
|
||||
+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,85 @@
|
||||
### 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)
|
||||
|
||||
@@ -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 = ./.;
|
||||
@@ -64,7 +65,7 @@
|
||||
postPatch = ''
|
||||
patchShebangs resources/niri-session
|
||||
substituteInPlace resources/niri.service \
|
||||
--replace-fail '/usr/bin' "$out/bin"
|
||||
--replace-fail 'ExecStart=niri' "ExecStart=$out/bin/niri"
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
@@ -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,6 +149,7 @@
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
NIRI_BUILD_COMMIT = revision;
|
||||
};
|
||||
|
||||
passthru = {
|
||||
@@ -156,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.7.2"
|
||||
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.11.3"
|
||||
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};
|
||||
|
||||
+35
-39
@@ -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;
|
||||
|
||||
@@ -132,6 +133,7 @@ pub enum Action {
|
||||
),
|
||||
ScreenshotWindow(
|
||||
#[knuffel(property(name = "write-to-disk"), default = true)] bool,
|
||||
#[knuffel(property(name = "show-pointer"), default = false)] bool,
|
||||
// Path; not settable from knuffel
|
||||
Option<String>,
|
||||
),
|
||||
@@ -139,6 +141,7 @@ pub enum Action {
|
||||
ScreenshotWindowById {
|
||||
id: u64,
|
||||
write_to_disk: bool,
|
||||
show_pointer: bool,
|
||||
path: Option<String>,
|
||||
},
|
||||
ToggleKeyboardShortcutsInhibit,
|
||||
@@ -354,6 +357,8 @@ pub enum Action {
|
||||
SetDynamicCastWindowById(u64),
|
||||
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
|
||||
ClearDynamicCastTarget,
|
||||
#[knuffel(skip)]
|
||||
StopCast(u64),
|
||||
ToggleOverview,
|
||||
OpenOverview,
|
||||
CloseOverview,
|
||||
@@ -364,7 +369,7 @@ pub enum Action {
|
||||
#[knuffel(skip)]
|
||||
UnsetWindowUrgent(u64),
|
||||
#[knuffel(skip)]
|
||||
LoadConfigFile,
|
||||
LoadConfigFile(#[knuffel(argument)] Option<String>),
|
||||
#[knuffel(skip)]
|
||||
MruAdvance {
|
||||
direction: MruDirection,
|
||||
@@ -407,15 +412,18 @@ impl From<niri_ipc::Action> for Action {
|
||||
niri_ipc::Action::ScreenshotWindow {
|
||||
id: None,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
} => Self::ScreenshotWindow(write_to_disk, path),
|
||||
} => Self::ScreenshotWindow(write_to_disk, show_pointer, path),
|
||||
niri_ipc::Action::ScreenshotWindow {
|
||||
id: Some(id),
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
} => Self::ScreenshotWindowById {
|
||||
id,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
},
|
||||
niri_ipc::Action::ToggleKeyboardShortcutsInhibit {} => {
|
||||
@@ -685,13 +693,14 @@ impl From<niri_ipc::Action> for Action {
|
||||
Self::SetDynamicCastMonitor(output)
|
||||
}
|
||||
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
|
||||
niri_ipc::Action::StopCast { session_id } => Self::StopCast(session_id),
|
||||
niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
|
||||
niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
|
||||
niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -761,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();
|
||||
|
||||
@@ -771,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1026,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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Debug {
|
||||
pub disable_direct_scanout: bool,
|
||||
pub keep_max_bpc_unchanged: bool,
|
||||
pub restrict_primary_scanout_to_matching_format: bool,
|
||||
pub force_disable_connectors_on_resume: bool,
|
||||
pub render_drm_device: Option<PathBuf>,
|
||||
pub ignored_drm_devices: Vec<PathBuf>,
|
||||
pub force_pipewire_invalid_modifier: bool,
|
||||
@@ -44,6 +45,8 @@ pub struct DebugPart {
|
||||
pub keep_max_bpc_unchanged: Option<Flag>,
|
||||
#[knuffel(child)]
|
||||
pub restrict_primary_scanout_to_matching_format: Option<Flag>,
|
||||
#[knuffel(child)]
|
||||
pub force_disable_connectors_on_resume: Option<Flag>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub render_drm_device: Option<PathBuf>,
|
||||
#[knuffel(children(name = "ignore-drm-device"), unwrap(argument))]
|
||||
@@ -81,6 +84,7 @@ impl MergeWith<DebugPart> for Debug {
|
||||
disable_direct_scanout,
|
||||
keep_max_bpc_unchanged,
|
||||
restrict_primary_scanout_to_matching_format,
|
||||
force_disable_connectors_on_resume,
|
||||
force_pipewire_invalid_modifier,
|
||||
emulate_zero_presentation_time,
|
||||
disable_resize_throttling,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
+119
-8
@@ -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),
|
||||
@@ -291,13 +295,71 @@ where
|
||||
}
|
||||
|
||||
"include" => {
|
||||
let path: PathBuf = utils::parse_arg_node("include", node, ctx)?;
|
||||
let base = ctx.get::<BasePath>().unwrap();
|
||||
let path = base.0.join(path);
|
||||
// Parse the path argument
|
||||
let mut iter_args = node.arguments.iter();
|
||||
let path_val = iter_args.next().ok_or_else(|| {
|
||||
DecodeError::missing(
|
||||
node,
|
||||
"additional argument for include path is required",
|
||||
)
|
||||
})?;
|
||||
let path: PathBuf = knuffel::traits::DecodeScalar::decode(path_val, ctx)?;
|
||||
|
||||
// Check for extra arguments
|
||||
if let Some(val) = iter_args.next() {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
&val.literal,
|
||||
"argument",
|
||||
"unexpected argument",
|
||||
));
|
||||
}
|
||||
|
||||
// Parse the optional property
|
||||
let mut optional = false;
|
||||
for (name, val) in &node.properties {
|
||||
match &***name {
|
||||
"optional" => {
|
||||
optional = knuffel::traits::DecodeScalar::decode(val, ctx)?;
|
||||
}
|
||||
name_str => {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
name,
|
||||
"property",
|
||||
format!("unexpected property `{}`", name_str.escape_default()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unexpected children
|
||||
for child in node.children() {
|
||||
ctx.emit_error(DecodeError::unexpected(
|
||||
child,
|
||||
"node",
|
||||
format!("unexpected node `{}`", child.node_name.escape_default()),
|
||||
));
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -369,10 +431,16 @@ where
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("failed to read included config from {path:?}: {err}"),
|
||||
));
|
||||
if optional && err.kind() == std::io::ErrorKind::NotFound {
|
||||
// Warn about missing optional includes
|
||||
warn!("optional include not found: {path:?}");
|
||||
} else {
|
||||
// Report all other errors normally
|
||||
ctx.emit_error(DecodeError::missing(
|
||||
node,
|
||||
format!("failed to read included config from {path:?}: {err}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,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
|
||||
}
|
||||
@@ -1043,6 +1112,7 @@ mod tests {
|
||||
map_to_output: Some(
|
||||
"eDP-1",
|
||||
),
|
||||
map_to_focused_output: true,
|
||||
left_handed: false,
|
||||
},
|
||||
touch: Touch {
|
||||
@@ -1566,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,
|
||||
@@ -1795,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: [
|
||||
@@ -1809,6 +1902,7 @@ mod tests {
|
||||
),
|
||||
),
|
||||
at_startup: None,
|
||||
layer: None,
|
||||
},
|
||||
],
|
||||
excludes: [],
|
||||
@@ -1829,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(
|
||||
@@ -2134,6 +2244,7 @@ mod tests {
|
||||
disable_direct_scanout: false,
|
||||
keep_max_bpc_unchanged: false,
|
||||
restrict_primary_scanout_to_matching_format: false,
|
||||
force_disable_connectors_on_resume: false,
|
||||
render_drm_device: Some(
|
||||
"/dev/dri/renderD129",
|
||||
),
|
||||
|
||||
@@ -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.0.4", 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"
|
||||
```
|
||||
|
||||
+134
-3
@@ -41,7 +41,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.11.0"
|
||||
//! niri-ipc = "=26.4.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -117,6 +117,8 @@ pub enum Request {
|
||||
ReturnError,
|
||||
/// Request information about the overview.
|
||||
OverviewState,
|
||||
/// Request information about screencasts.
|
||||
Casts,
|
||||
}
|
||||
|
||||
/// Reply from niri to client.
|
||||
@@ -161,6 +163,8 @@ pub enum Response {
|
||||
OutputConfigChanged(OutputConfigChanged),
|
||||
/// Information about the overview.
|
||||
OverviewState(Overview),
|
||||
/// Information about screencasts.
|
||||
Casts(Vec<Cast>),
|
||||
}
|
||||
|
||||
/// Overview information.
|
||||
@@ -264,6 +268,13 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
write_to_disk: bool,
|
||||
|
||||
/// Whether to include the mouse pointer in the screenshot.
|
||||
///
|
||||
/// 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).
|
||||
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = false))]
|
||||
show_pointer: bool,
|
||||
|
||||
/// Path to save the screenshot to.
|
||||
///
|
||||
/// The path must be absolute, otherwise an error is returned.
|
||||
@@ -429,7 +440,7 @@ pub enum Action {
|
||||
},
|
||||
/// Consume the window to the right into the focused column.
|
||||
ConsumeWindowIntoColumn {},
|
||||
/// Expel the focused window from the column.
|
||||
/// Expel the bottom window from the focused column.
|
||||
ExpelWindowFromColumn {},
|
||||
/// Swap focused window with one to the right.
|
||||
SwapWindowRight {},
|
||||
@@ -887,6 +898,16 @@ pub enum Action {
|
||||
},
|
||||
/// Clear the dynamic cast target, making it show nothing.
|
||||
ClearDynamicCastTarget {},
|
||||
/// Stop a PipeWire screencast.
|
||||
///
|
||||
/// wlr-screencopy screencasts cannot currently be stopped via IPC.
|
||||
StopCast {
|
||||
/// Session ID of the screencast to stop.
|
||||
///
|
||||
/// If the session has multiple screencast streams, this will stop all of them.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
session_id: u64,
|
||||
},
|
||||
/// Toggle (open/close) the Overview.
|
||||
ToggleOverview {},
|
||||
/// Open the Overview.
|
||||
@@ -915,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.
|
||||
@@ -1466,6 +1493,78 @@ pub struct LayerSurface {
|
||||
pub keyboard_interactivity: LayerSurfaceKeyboardInteractivity,
|
||||
}
|
||||
|
||||
/// A screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Cast {
|
||||
/// Stream ID of the screencast that uniquely identifies it.
|
||||
pub stream_id: u64,
|
||||
/// Session ID of the screencast.
|
||||
///
|
||||
/// A session can have multiple screencast streams. Then multiple `Cast`s will have the same
|
||||
/// `session_id`. Though, usually there's only one stream per session.
|
||||
///
|
||||
/// Do not confuse `session_id` with [`stream_id`](Self::stream_id).
|
||||
pub session_id: u64,
|
||||
/// Kind of this screencast.
|
||||
pub kind: CastKind,
|
||||
/// Target being captured.
|
||||
pub target: CastTarget,
|
||||
/// Whether this is a Dynamic Cast Target screencast.
|
||||
///
|
||||
/// Meaning that actions like `SetDynamicCastWindow` will act on this screencast.
|
||||
///
|
||||
/// Keep in mind that the target can change even if this is `false`.
|
||||
pub is_dynamic_target: bool,
|
||||
/// Whether the cast is currently streaming frames.
|
||||
///
|
||||
/// This can be `false` for example when switching away to a different scene in OBS, which
|
||||
/// pauses the stream.
|
||||
pub is_active: bool,
|
||||
/// Process ID of the screencast consumer, if known.
|
||||
///
|
||||
/// Currently, only wlr-screencopy screencasts can have a pid.
|
||||
pub pid: Option<i32>,
|
||||
/// PipeWire node ID of the screencast stream.
|
||||
///
|
||||
/// This is `None` for wlr-screencopy casts, and also for PipeWire casts before the node is
|
||||
/// created (when the cast is just starting up).
|
||||
pub pw_node_id: Option<u32>,
|
||||
}
|
||||
|
||||
/// Kind of screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum CastKind {
|
||||
/// PipeWire screencast, typically via xdg-desktop-portal-gnome.
|
||||
PipeWire,
|
||||
/// wlr-screencopy protocol screencast.
|
||||
///
|
||||
/// Tools like wf-recorder, and the xdg-desktop-portal-wlr portal.
|
||||
///
|
||||
/// Only wlr-screencopy with damage tracking is reported here. Screencopy without damage is
|
||||
/// treated as a regular screenshot and not reported as a screencast.
|
||||
WlrScreencopy,
|
||||
}
|
||||
|
||||
/// Target of a screencast.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum CastTarget {
|
||||
/// The target is not yet set, or was cleared.
|
||||
Nothing {},
|
||||
/// Casting an output.
|
||||
Output {
|
||||
/// Name of the screencasted output.
|
||||
name: String,
|
||||
},
|
||||
/// Casting a window.
|
||||
Window {
|
||||
/// ID of the screencasted window.
|
||||
id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// A compositor event.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -1588,6 +1687,24 @@ pub enum Event {
|
||||
/// be converted to a `String` (e.g. contained invalid UTF-8 bytes).
|
||||
path: Option<String>,
|
||||
},
|
||||
/// The screencasts have changed.
|
||||
CastsChanged {
|
||||
/// The new screencast information.
|
||||
///
|
||||
/// This configuration completely replaces the previous configuration. I.e. if any casts
|
||||
/// are missing from here, then they were stopped.
|
||||
casts: Vec<Cast>,
|
||||
},
|
||||
/// A screencast started, or an existing cast changed.
|
||||
CastStartedOrChanged {
|
||||
/// The cast that started or changed.
|
||||
cast: Cast,
|
||||
},
|
||||
/// A screencast stopped.
|
||||
CastStopped {
|
||||
/// Stream ID of the stopped screencast.
|
||||
stream_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Duration> for Timestamp {
|
||||
@@ -1751,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;
|
||||
|
||||
|
||||
+37
-1
@@ -9,7 +9,7 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Event, KeyboardLayouts, Window, Workspace};
|
||||
use crate::{Cast, Event, KeyboardLayouts, Window, Workspace};
|
||||
|
||||
/// Part of the state communicated via the event stream.
|
||||
pub trait EventStreamStatePart {
|
||||
@@ -46,6 +46,9 @@ pub struct EventStreamState {
|
||||
|
||||
/// State of the config.
|
||||
pub config: ConfigState,
|
||||
|
||||
/// State of screencasts.
|
||||
pub casts: CastsState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
@@ -83,6 +86,13 @@ pub struct ConfigState {
|
||||
pub failed: bool,
|
||||
}
|
||||
|
||||
/// The casts state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CastsState {
|
||||
/// Map from a stream id to the screencast.
|
||||
pub casts: HashMap<u64, Cast>,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
@@ -91,6 +101,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events.extend(self.overview.replicate());
|
||||
events.extend(self.config.replicate());
|
||||
events.extend(self.casts.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
@@ -100,6 +111,7 @@ impl EventStreamStatePart for EventStreamState {
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
let event = self.overview.apply(event)?;
|
||||
let event = self.config.apply(event)?;
|
||||
let event = self.casts.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
@@ -285,3 +297,27 @@ impl EventStreamStatePart for ConfigState {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for CastsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let casts = self.casts.values().cloned().collect();
|
||||
vec![Event::CastsChanged { casts }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::CastsChanged { casts } => {
|
||||
self.casts = casts.into_iter().map(|c| (c.stream_id, c)).collect();
|
||||
}
|
||||
Event::CastStartedOrChanged { cast } => {
|
||||
self.casts.insert(cast.stream_id, cast);
|
||||
}
|
||||
Event::CastStopped { stream_id } => {
|
||||
let cast = self.casts.remove(&stream_id);
|
||||
cast.expect("stopped cast was missing from the map");
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.7", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.11.0", path = ".." }
|
||||
niri-config = { version = "25.11.0", path = "../niri-config" }
|
||||
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
|
||||
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
|
||||
|
||||
@@ -89,11 +89,8 @@ impl TestCase for GradientArea {
|
||||
1.,
|
||||
1.,
|
||||
);
|
||||
rv.extend(
|
||||
self.border
|
||||
.render(renderer, g_loc)
|
||||
.map(|elem| Box::new(elem) as _),
|
||||
);
|
||||
self.border
|
||||
.render(renderer, g_loc, &mut |elem| rv.push(Box::new(elem) as _));
|
||||
|
||||
rv.extend(
|
||||
[BorderRenderElement::new(
|
||||
|
||||
@@ -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;
|
||||
@@ -268,12 +268,17 @@ impl TestCase for Layout {
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
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_elements(renderer, RenderTarget::Output, true)
|
||||
.flat_map(|(_, _, iter)| iter)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
.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;
|
||||
@@ -119,9 +120,18 @@ impl TestCase for Tile {
|
||||
true,
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
);
|
||||
|
||||
let mut rv = Vec::new();
|
||||
let ctx = RenderCtx {
|
||||
renderer,
|
||||
target: RenderTarget::Output,
|
||||
xray: None,
|
||||
};
|
||||
let xray_pos = XrayPos::new(location, 1.);
|
||||
self.tile
|
||||
.render(renderer, location, true, RenderTarget::Output)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
.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};
|
||||
@@ -52,16 +52,16 @@ impl TestCase for Window {
|
||||
.to_f64()
|
||||
.downscale(2.);
|
||||
|
||||
let mut rv = Vec::new();
|
||||
let ctx = RenderCtx {
|
||||
renderer,
|
||||
target: RenderTarget::Output,
|
||||
xray: None,
|
||||
};
|
||||
self.window
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
1.,
|
||||
RenderTarget::Output,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
.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")?;
|
||||
}
|
||||
}
|
||||
@@ -255,7 +263,8 @@ mod imp {
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
|
||||
@extends gtk::Widget;
|
||||
@extends gtk::Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl SmithayView {
|
||||
|
||||
@@ -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, SplitElements};
|
||||
use niri::render_helpers::RenderCtx;
|
||||
use niri::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
@@ -149,36 +149,29 @@ impl LayoutElement for TestWindow {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
_ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
_target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
SplitElements {
|
||||
normal: vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
push(
|
||||
SolidColorRenderElement::from_buffer(&inner.buffer, location, alpha, Kind::Unspecified)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
location
|
||||
- Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
],
|
||||
popups: vec![],
|
||||
}
|
||||
);
|
||||
push(
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&inner.csd_shadow_buffer,
|
||||
location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)).to_f64(),
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
fn request_size(
|
||||
|
||||
+4
-1
@@ -60,7 +60,7 @@ SourceLicense: GPL-3.0-or-later
|
||||
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.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 }}}
|
||||
|
||||
@@ -85,6 +85,9 @@ BuildRequires: mesa-libEGL
|
||||
Requires: mesa-dri-drivers
|
||||
Requires: mesa-libEGL
|
||||
|
||||
# Loaded through dlopen
|
||||
Requires: libwayland-server
|
||||
|
||||
# Integrated Xwayland support. Not packaged on EPEL
|
||||
%if 0%{?fedora}
|
||||
Requires: xwayland-satellite >= 0.7
|
||||
|
||||
@@ -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,14 +550,23 @@ 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; }
|
||||
|
||||
// While maximize-column leaves gaps and borders around the window,
|
||||
// maximize-window-to-edges doesn't: the window expands to the edges of the screen.
|
||||
// This bind corresponds to normal window maximizing,
|
||||
// e.g. by double-clicking on the titlebar.
|
||||
Mod+M { maximize-window-to-edges; }
|
||||
|
||||
// Expand the focused column to space not taken up by other fully visible columns.
|
||||
// Makes the column "fill the rest of the space".
|
||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
depends-on = dbus
|
||||
after = niri-shutdown
|
||||
chain-to = niri-shutdown
|
||||
options: always-chain
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
ready-notification = pipevar:NOTIFY_FD
|
||||
logfile = $HOME/.local/share/niri/niri.log
|
||||
depends-on: dbus
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
type = scripted
|
||||
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
|
||||
restart = false
|
||||
@@ -0,0 +1,6 @@
|
||||
type = internal
|
||||
restart = false
|
||||
depends-on: niri
|
||||
waits-for.d: $XDG_CONFIG_HOME/dinit.d/niri.d/
|
||||
waits-for.d: $HOME/.config/dinit.d/niri.d/
|
||||
waits-for.d: /etc/dinit.d/user/niri.d/
|
||||
+26
-2
@@ -59,13 +59,37 @@ elif hash dinitctl >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri >/dev/null 2>&1; then
|
||||
if dinitctl --quiet --user is-started niri 2>/dev/null; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Import the login manager environment into dinit
|
||||
# Might not work correctly for multiline variable names, but
|
||||
# it is reasonable to assume there are none
|
||||
awk 'BEGIN{for(v in ENVIRON) if (v != "AWKPATH" && v != "AWKLIBPATH") print v}' 2>/dev/null | xargs dinitctl --quiet --user setenv 2>/dev/null
|
||||
|
||||
# Usually the dbus service would start as niri's dependency and inherit
|
||||
# environment from dinit, but in case it has already started we need
|
||||
# to update its environment.
|
||||
if hash dbus-update-activation-environment >/dev/null 2>&1; then
|
||||
dbus-update-activation-environment --all >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Create the directory for the logfile, if doesn't exist
|
||||
mkdir --parents $HOME/.local/share/niri
|
||||
# Start niri
|
||||
dinitctl --user start niri
|
||||
dinitctl --quiet --user start niri.target 2>&1
|
||||
|
||||
# Wait for termination
|
||||
dinit-monitor --user --initial -c $'sh -c "
|
||||
if [ "%s" = "stopped" ] || [ "%s" = "failed" ]; then
|
||||
ppid=$(ps -o ppid= -p $$)
|
||||
kill $ppid
|
||||
fi"' niri >/dev/null 2>&1
|
||||
|
||||
# Unset environment that we've set.
|
||||
dinitctl --quiet --user unsetenv WAYLAND_DISPLAY DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET 2>/dev/null
|
||||
else
|
||||
echo "No systemd or dinit detected, please use niri --session instead."
|
||||
fi
|
||||
|
||||
@@ -11,4 +11,4 @@ Before=xdg-desktop-autostart.target
|
||||
[Service]
|
||||
Slice=session.slice
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/niri --session
|
||||
ExecStart=niri --session
|
||||
|
||||
+248
-135
@@ -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>>,
|
||||
@@ -434,12 +431,21 @@ impl Tty {
|
||||
.unwrap();
|
||||
|
||||
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||||
unsafe { init_libinput_plugin_system(&libinput) };
|
||||
{
|
||||
let _span = tracy_client::span!("Libinput::udev_assign_seat");
|
||||
libinput.udev_assign_seat(&seat_name)
|
||||
}
|
||||
.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| {
|
||||
@@ -486,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,
|
||||
@@ -499,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();
|
||||
|
||||
@@ -549,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:?}");
|
||||
}
|
||||
@@ -596,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
|
||||
@@ -646,7 +654,16 @@ impl Tty {
|
||||
|
||||
// It hasn't been removed, update its state as usual.
|
||||
let device = self.devices.get_mut(&node).unwrap();
|
||||
if let Err(err) = device.drm.activate(false) {
|
||||
|
||||
// Someone on an old device hit what seems to be a driver bug without this:
|
||||
// https://github.com/niri-wm/niri/issues/3048
|
||||
let force_disable = self
|
||||
.config
|
||||
.borrow()
|
||||
.debug
|
||||
.force_disable_connectors_on_resume;
|
||||
|
||||
if let Err(err) = device.drm.activate(force_disable) {
|
||||
warn!("error activating DRM device: {err:?}");
|
||||
}
|
||||
if let Some(lease_state) = &mut device.drm_lease_state {
|
||||
@@ -689,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:?}");
|
||||
}
|
||||
@@ -799,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();
|
||||
@@ -969,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.
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1055,6 +1108,7 @@ impl Tty {
|
||||
if let Err(err) = surface.compositor.reset_state() {
|
||||
warn!("error resetting DrmCompositor state: {err:?}");
|
||||
}
|
||||
surface.compositor.reset_buffers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1382,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(),
|
||||
@@ -1412,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(),
|
||||
@@ -1666,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:?}"
|
||||
@@ -1828,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 {
|
||||
@@ -2225,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;
|
||||
}
|
||||
@@ -3324,6 +3385,50 @@ fn make_output_name(
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the libinput plugin system.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function must be called before libinput iterates through the devices, i.e. before
|
||||
/// libinput_udev_assign_seat() or the first call to libinput_path_add_device().
|
||||
unsafe fn init_libinput_plugin_system(libinput: &Libinput) {
|
||||
#[cfg(have_libinput_plugin_system)]
|
||||
unsafe {
|
||||
use std::ffi::{c_char, c_int, CString};
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
use directories::BaseDirs;
|
||||
use input::ffi::libinput;
|
||||
use input::AsRaw as _;
|
||||
|
||||
extern "C" {
|
||||
fn libinput_plugin_system_append_path(libinput: *const libinput, path: *const c_char);
|
||||
fn libinput_plugin_system_append_default_paths(libinput: *const libinput);
|
||||
fn libinput_plugin_system_load_plugins(
|
||||
libinput: *const libinput,
|
||||
flags: c_int,
|
||||
) -> c_int;
|
||||
}
|
||||
const LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE: c_int = 0;
|
||||
let libinput = libinput.as_raw();
|
||||
|
||||
// Also load plugins from $XDG_CONFIG_HOME/libinput/plugins.
|
||||
if let Some(dirs) = BaseDirs::new() {
|
||||
let mut plugins_dir = dirs.config_dir().to_path_buf();
|
||||
plugins_dir.push("libinput");
|
||||
plugins_dir.push("plugins");
|
||||
if let Ok(plugins_dir) = CString::new(plugins_dir.into_os_string().into_vec()) {
|
||||
libinput_plugin_system_append_path(libinput, plugins_dir.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
libinput_plugin_system_append_default_paths(libinput);
|
||||
libinput_plugin_system_load_plugins(libinput, LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
|
||||
}
|
||||
#[cfg(not(have_libinput_plugin_system))]
|
||||
let _ = libinput;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
@@ -3347,30 +3452,32 @@ mod tests {
|
||||
hsync_polarity: HSyncPolarity::NHSync,
|
||||
vsync_polarity: VSyncPolarity::PVSync,
|
||||
};
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline1).unwrap(), @"Mode {
|
||||
name: \"1920x1080@59.96\",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline1).unwrap(), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@59.96",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
let modeline2 = Modeline {
|
||||
clock: 452.5,
|
||||
hdisplay: 1920,
|
||||
@@ -3384,82 +3491,88 @@ mod tests {
|
||||
hsync_polarity: HSyncPolarity::NHSync,
|
||||
vsync_polarity: VSyncPolarity::PVSync,
|
||||
};
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline2).unwrap(), @"Mode {
|
||||
name: \"1920x1080@143.88\",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline2).unwrap(), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@143.88",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calc_cvt() {
|
||||
// Crosschecked with other calculators like the cvt commandline utility.
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 60.0), @"Mode {
|
||||
name: \"1920x1080@59.96\",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 144.0), @"Mode {
|
||||
name: \"1920x1080@143.88\",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 60.0), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@59.96",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 144.0), @r#"
|
||||
Mode {
|
||||
name: "1920x1080@143.88",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
+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 {
|
||||
|
||||
@@ -107,6 +107,8 @@ pub enum Msg {
|
||||
RequestError,
|
||||
/// Print the overview state.
|
||||
OverviewState,
|
||||
/// List screencasts.
|
||||
Casts,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::ValueEnum)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Deserialize;
|
||||
@@ -11,6 +11,7 @@ use zbus::{fdo, interface, ObjectServer};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::{CastSessionId, CastStreamId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenCast {
|
||||
@@ -22,7 +23,7 @@ pub struct ScreenCast {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
id: usize,
|
||||
id: CastSessionId,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -30,7 +31,7 @@ pub struct Session {
|
||||
stopped: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CursorMode {
|
||||
#[default]
|
||||
Hidden = 0,
|
||||
@@ -58,12 +59,10 @@ struct RecordWindowProperties {
|
||||
_is_recording: Option<bool>,
|
||||
}
|
||||
|
||||
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
id: CastStreamId,
|
||||
session_id: CastSessionId,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
was_started: Arc<AtomicBool>,
|
||||
@@ -94,14 +93,14 @@ struct StreamParameters {
|
||||
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
stream_id: usize,
|
||||
session_id: CastSessionId,
|
||||
stream_id: CastStreamId,
|
||||
target: StreamTargetId,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
},
|
||||
StopCast {
|
||||
session_id: usize,
|
||||
session_id: CastSessionId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -118,9 +117,8 @@ impl ScreenCast {
|
||||
));
|
||||
}
|
||||
|
||||
static NUMBER: AtomicUsize = AtomicUsize::new(0);
|
||||
let session_id = NUMBER.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{session_id}");
|
||||
let session_id = CastSessionId::next();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id.get());
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone());
|
||||
@@ -207,8 +205,8 @@ impl Session {
|
||||
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||
}
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let stream_id = CastStreamId::next();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{}", stream_id.get());
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
@@ -244,8 +242,8 @@ impl Session {
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(?properties, "record_window");
|
||||
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let stream_id = CastStreamId::next();
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{}", stream_id.get());
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
@@ -337,7 +335,7 @@ impl Start for ScreenCast {
|
||||
|
||||
impl Session {
|
||||
pub fn new(
|
||||
id: usize,
|
||||
id: CastSessionId,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
@@ -361,8 +359,8 @@ impl Drop for Session {
|
||||
|
||||
impl Stream {
|
||||
fn new(
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
id: CastStreamId,
|
||||
session_id: CastSessionId,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use anyhow::bail;
|
||||
use smithay::reexports::calloop::LoopHandle;
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct PipeWire;
|
||||
pub struct Cast;
|
||||
|
||||
impl PipeWire {
|
||||
pub fn new(_event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
||||
bail!("PipeWire support is disabled (see \"xdp-gnome-screencast\" feature)");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
+28
-12
@@ -62,10 +62,6 @@ impl CompositorHandler for State {
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.backend.early_import(surface);
|
||||
|
||||
if is_sync_subsurface(surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut root_surface = surface.clone();
|
||||
while let Some(parent) = get_parent(&root_surface) {
|
||||
root_surface = parent;
|
||||
@@ -76,6 +72,10 @@ impl CompositorHandler for State {
|
||||
.root_surface
|
||||
.insert(surface.clone(), root_surface.clone());
|
||||
|
||||
if is_sync_subsurface(surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
if surface == &root_surface {
|
||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
||||
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+69
-37
@@ -1,3 +1,4 @@
|
||||
pub mod background_effect;
|
||||
mod compositor;
|
||||
mod layer_shell;
|
||||
mod xdg_shell;
|
||||
@@ -13,16 +14,16 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::dnd::{self, DnDGrab, DndGrabHandler, DndTarget};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, Focus, PointerHandle};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Point, Rectangle};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
@@ -41,8 +42,7 @@ use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
use smithay::wayland::selection::data_device::{
|
||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||
ServerDndGrabHandler,
|
||||
set_data_device_focus, DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler,
|
||||
};
|
||||
use smithay::wayland::selection::ext_data_control::{
|
||||
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
|
||||
@@ -314,32 +314,60 @@ impl DataDeviceHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientDndGrabHandler for State {
|
||||
fn started(
|
||||
impl WaylandDndGrabHandler for State {
|
||||
fn dnd_requested<S: dnd::Source>(
|
||||
&mut self,
|
||||
_source: Option<WlDataSource>,
|
||||
source: S,
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
seat: Seat<Self>,
|
||||
serial: Serial,
|
||||
type_: dnd::GrabType,
|
||||
) {
|
||||
self.niri.dnd_icon = icon.map(|surface| DndIcon {
|
||||
surface,
|
||||
offset: Point::new(0, 0),
|
||||
});
|
||||
|
||||
match type_ {
|
||||
dnd::GrabType::Pointer => {
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
let start_data = pointer.grab_start_data().unwrap();
|
||||
let grab =
|
||||
DnDGrab::new_pointer(&self.niri.display_handle, start_data, source, seat);
|
||||
pointer.set_grab(self, grab, serial, Focus::Keep);
|
||||
}
|
||||
dnd::GrabType::Touch => {
|
||||
let touch = seat.get_touch().unwrap();
|
||||
let start_data = touch.grab_start_data().unwrap();
|
||||
let grab = DnDGrab::new_touch(&self.niri.display_handle, start_data, source, seat);
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||
impl DndGrabHandler for State {
|
||||
fn dropped(
|
||||
&mut self,
|
||||
target: Option<DndTarget<'_, Self>>,
|
||||
validated: bool,
|
||||
_seat: Seat<Self>,
|
||||
location: Point<f64, Logical>,
|
||||
) {
|
||||
let target: Option<&WlSurface> = target.map(DndTarget::into_inner);
|
||||
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.
|
||||
let mut activate_output = true;
|
||||
if let Some(target) = validated.then_some(target).flatten() {
|
||||
let root = self.niri.find_root_shell_surface(&target);
|
||||
let root = self.niri.find_root_shell_surface(target);
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&root) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
@@ -349,29 +377,29 @@ impl ClientDndGrabHandler for State {
|
||||
}
|
||||
|
||||
if activate_output {
|
||||
// Find the output from cursor coordinates.
|
||||
//
|
||||
// FIXME: uhhh, we can't actually properly tell if the DnD comes from pointer or touch,
|
||||
// and if it comes from touch, then what the coordinates are. Need to pass more
|
||||
// parameters from Smithay I guess.
|
||||
//
|
||||
// Assume that hidden pointer means touch DnD.
|
||||
if self.niri.pointer_visibility.is_visible() {
|
||||
// We can't even get the current pointer location because it's locked (we're deep
|
||||
// in the grab call stack here). So use the last known one.
|
||||
if let Some(output) = &self.niri.pointer_contents.output {
|
||||
self.niri.layout.focus_output(output);
|
||||
}
|
||||
// Find the output from drop coordinates.
|
||||
if let Some((output, _)) = self.niri.output_under(location) {
|
||||
let output = output.clone();
|
||||
self.niri.layout.focus_output(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
|
||||
trace!("dnd cancelled");
|
||||
|
||||
self.niri.on_maybe_dnd_ended();
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerDndGrabHandler for State {}
|
||||
impl crate::niri::Niri {
|
||||
fn on_maybe_dnd_ended(&mut self) {
|
||||
self.layout.dnd_end();
|
||||
self.dnd_icon = None;
|
||||
// FIXME: more granular
|
||||
self.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
delegate_data_device!(State);
|
||||
|
||||
@@ -444,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;
|
||||
};
|
||||
@@ -529,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),
|
||||
@@ -605,14 +635,16 @@ 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() {
|
||||
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
|
||||
trace!("screencopy manager destroyed already");
|
||||
return;
|
||||
};
|
||||
queue.push(screencopy);
|
||||
self.niri.screencopy_state.push(manager, screencopy);
|
||||
} else {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
if let Err(err) = self
|
||||
|
||||
+54
-42
@@ -24,7 +24,6 @@ use smithay::wayland::compositor::{
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::{self, Layer};
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
@@ -85,7 +84,7 @@ impl XdgShellHandler for State {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
let is_dnd_grab = Self::is_dnd_grab(grab.as_any());
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
@@ -105,7 +104,7 @@ impl XdgShellHandler for State {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
let is_dnd_grab = Self::is_dnd_grab(grab.as_any());
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
@@ -134,13 +133,13 @@ impl XdgShellHandler for State {
|
||||
|
||||
match &start_data {
|
||||
PointerOrTouchStartData::Pointer(_) => {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true) {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true, None) {
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
}
|
||||
PointerOrTouchStartData::Touch(_) => {
|
||||
let touch = self.niri.seat.get_touch().unwrap();
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true) {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true, None) {
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
@@ -268,15 +267,6 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
|
||||
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
||||
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
|
||||
// a grab. It will likely need refactors in Smithay to support properly since grabs just
|
||||
// replace each other.
|
||||
// FIXME: do this properly.
|
||||
if self.niri.seat.input_method().keyboard_grabbed() {
|
||||
trace!("ignoring popup grab because IME has keyboard grabbed");
|
||||
return;
|
||||
}
|
||||
|
||||
let popup = PopupKind::Xdg(surface);
|
||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||
trace!("ignoring popup grab because no root surface");
|
||||
@@ -374,25 +364,30 @@ impl XdgShellHandler for State {
|
||||
let keyboard = seat.get_keyboard().unwrap();
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
let can_receive_keyboard_focus = self
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.and_then(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
|
||||
})
|
||||
.unwrap_or(true);
|
||||
// Smithay cannot do overlapping grabs, so if we have an IME keyboard grab, don't overwrite
|
||||
// it with a popup keyboard grab. This makes the popup menu work in Telegram while an IME
|
||||
// is active (otherwise it hits the grab mismatch check below).
|
||||
//
|
||||
// The second check is for layer surfaces that can't receive keyboard focus, without it
|
||||
// popups don't work properly in Waybar (GTK 3).
|
||||
let can_receive_keyboard_focus = !self.niri.seat.input_method().keyboard_grabbed()
|
||||
&& self
|
||||
.niri
|
||||
.layout
|
||||
.active_output()
|
||||
.and_then(|output| {
|
||||
layer_map_for_output(output)
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.map(|layer_surface| layer_surface.can_receive_keyboard_focus())
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
let keyboard_grab_mismatches = keyboard.is_grabbed()
|
||||
&& !(keyboard.has_grab(serial)
|
||||
|| grab
|
||||
.previous_serial()
|
||||
.map_or(true, |s| keyboard.has_grab(s)));
|
||||
|| grab.previous_serial().is_none_or(|s| keyboard.has_grab(s)));
|
||||
let pointer_grab_mismatches = pointer.is_grabbed()
|
||||
&& !(pointer.has_grab(serial)
|
||||
|| grab.previous_serial().map_or(true, |s| pointer.has_grab(s)));
|
||||
|| grab.previous_serial().is_none_or(|s| pointer.has_grab(s)));
|
||||
if (can_receive_keyboard_focus && keyboard_grab_mismatches) || pointer_grab_mismatches {
|
||||
trace!("ignoring popup grab because of current grab mismatch");
|
||||
grab.ungrab(PopupUngrabStrategy::All);
|
||||
@@ -617,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
|
||||
@@ -851,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();
|
||||
@@ -868,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.
|
||||
@@ -1256,7 +1257,7 @@ impl State {
|
||||
let mut target = self.niri.layout.popup_target_rect(window);
|
||||
target.loc -= get_popup_toplevel_coords(popup).to_f64();
|
||||
|
||||
self.position_popup_within_rect(popup, target);
|
||||
self.position_popup_within_rect(popup, target, true);
|
||||
}
|
||||
|
||||
pub fn unconstrain_layer_shell_popup(
|
||||
@@ -1290,14 +1291,26 @@ impl State {
|
||||
target.loc -= layer_geo.loc;
|
||||
target.loc -= get_popup_toplevel_coords(popup);
|
||||
|
||||
self.position_popup_within_rect(popup, target.to_f64());
|
||||
// Don't add padding to layer-shell popups. It's not really needed, and it's unexpected.
|
||||
self.position_popup_within_rect(popup, target.to_f64(), false);
|
||||
}
|
||||
|
||||
fn position_popup_within_rect(&self, popup: &PopupKind, target: Rectangle<f64, Logical>) {
|
||||
fn position_popup_within_rect(
|
||||
&self,
|
||||
popup: &PopupKind,
|
||||
target: Rectangle<f64, Logical>,
|
||||
padding: bool,
|
||||
) {
|
||||
match popup {
|
||||
PopupKind::Xdg(popup) => {
|
||||
popup.with_pending_state(|state| {
|
||||
state.geometry = unconstrain_with_padding(state.positioner, target);
|
||||
state.geometry = if padding {
|
||||
unconstrain_with_padding(state.positioner, target)
|
||||
} else {
|
||||
state
|
||||
.positioner
|
||||
.get_unconstrained_geometry(target.to_i32_round())
|
||||
};
|
||||
});
|
||||
}
|
||||
PopupKind::InputMethod(popup) => {
|
||||
@@ -1430,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;
|
||||
};
|
||||
@@ -1466,7 +1479,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
|
||||
span.record("serial", format!("{serial:?}"));
|
||||
}
|
||||
|
||||
trace!("taking pending transaction");
|
||||
// trace!("taking pending transaction");
|
||||
if let Some(transaction) = mapped.take_pending_transaction(serial) {
|
||||
// Transaction can be already completed if it ran past the deadline.
|
||||
let disable = state.niri.config.borrow().debug.disable_transactions;
|
||||
@@ -1532,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| {
|
||||
|
||||
+105
-38
@@ -19,6 +19,7 @@ use smithay::backend::input::{
|
||||
TabletToolTipState, TouchEvent,
|
||||
};
|
||||
use smithay::backend::libinput::LibinputInputBackend;
|
||||
use smithay::input::dnd::DnDGrab;
|
||||
use smithay::input::keyboard::{keysyms, FilterResult, Keysym, Layout, ModifiersState};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, Focus, GestureHoldBeginEvent,
|
||||
@@ -31,14 +32,17 @@ use smithay::input::touch::{
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
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")]
|
||||
@@ -49,7 +53,7 @@ use crate::niri::{CastTarget, PointerVisibility, State};
|
||||
use crate::ui::mru::{WindowMru, WindowMruUi};
|
||||
use crate::ui::screenshot_ui::ScreenshotUi;
|
||||
use crate::utils::spawning::{spawn, spawn_sh};
|
||||
use crate::utils::{center, get_monotonic_time, ResizeEdge};
|
||||
use crate::utils::{center, get_monotonic_time, CastSessionId, ResizeEdge};
|
||||
|
||||
pub mod backend_ext;
|
||||
pub mod move_grab;
|
||||
@@ -291,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()) {
|
||||
@@ -486,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 {
|
||||
@@ -741,7 +744,7 @@ impl State {
|
||||
self.open_screenshot_ui(show_cursor, path);
|
||||
self.niri.cancel_mru();
|
||||
}
|
||||
Action::ScreenshotWindow(write_to_disk, path) => {
|
||||
Action::ScreenshotWindow(write_to_disk, show_pointer, path) => {
|
||||
let focus = self.niri.layout.focus_with_output();
|
||||
if let Some((mapped, output)) = focus {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
@@ -750,6 +753,7 @@ impl State {
|
||||
output,
|
||||
mapped,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
) {
|
||||
warn!("error taking screenshot: {err:?}");
|
||||
@@ -760,6 +764,7 @@ impl State {
|
||||
Action::ScreenshotWindowById {
|
||||
id,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
} => {
|
||||
let mut windows = self.niri.layout.windows();
|
||||
@@ -772,6 +777,7 @@ impl State {
|
||||
output,
|
||||
mapped,
|
||||
write_to_disk,
|
||||
show_pointer,
|
||||
path,
|
||||
) {
|
||||
warn!("error taking screenshot: {err:?}");
|
||||
@@ -2230,13 +2236,15 @@ impl State {
|
||||
Some(name) => self.niri.output_by_name_match(&name),
|
||||
};
|
||||
if let Some(output) = output {
|
||||
let output = output.downgrade();
|
||||
self.set_dynamic_cast_target(CastTarget::Output(output));
|
||||
self.set_dynamic_cast_target(CastTarget::output(output));
|
||||
}
|
||||
}
|
||||
Action::ClearDynamicCastTarget => {
|
||||
self.set_dynamic_cast_target(CastTarget::Nothing);
|
||||
}
|
||||
Action::StopCast(session_id) => {
|
||||
self.niri.stop_cast(CastSessionId::from(session_id));
|
||||
}
|
||||
Action::ToggleOverview => {
|
||||
self.niri.layout.toggle_overview();
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -2285,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 => {
|
||||
@@ -2454,6 +2462,35 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
// Warp pointer across the screen during the spatial movement grabs.
|
||||
let spatial_grab = pointer.with_grab(|_, grab| {
|
||||
let grab = grab.as_any();
|
||||
if let Some(grab) = grab.downcast_ref::<SpatialMovementGrab>() {
|
||||
if let Some(output) = grab.view_offset_output() {
|
||||
return Some((output.clone(), true));
|
||||
} else if let Some(output) = grab.workspace_switch_output() {
|
||||
return Some((output.clone(), false));
|
||||
}
|
||||
} else if let Some(grab) = grab.downcast_ref::<MoveGrab>() {
|
||||
if let Some(output) = grab.view_offset_output() {
|
||||
return Some((output.clone(), true));
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
if let Some((output, horizontal)) = spatial_grab.flatten() {
|
||||
if let Some(geo) = self.niri.global_space.output_geometry(&output) {
|
||||
let geo = geo.to_f64();
|
||||
if horizontal {
|
||||
new_pos.x = (new_pos.x - geo.loc.x).rem_euclid(geo.size.w) + geo.loc.x;
|
||||
new_pos.y = new_pos.y.clamp(geo.loc.y, geo.loc.y + geo.size.h - 1.);
|
||||
} else {
|
||||
new_pos.x = new_pos.x.clamp(geo.loc.x, geo.loc.x + geo.size.w - 1.);
|
||||
new_pos.y = (new_pos.y - geo.loc.y).rem_euclid(geo.size.h) + geo.loc.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.niri
|
||||
.global_space
|
||||
@@ -2583,10 +2620,9 @@ impl State {
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
|
||||
// Inform the layout of an ongoing DnD operation.
|
||||
let mut is_dnd_grab = false;
|
||||
pointer.with_grab(|_, grab| {
|
||||
is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
});
|
||||
let is_dnd_grab = pointer
|
||||
.with_grab(|_, grab| Self::is_dnd_grab(grab.as_any()))
|
||||
.unwrap_or(false);
|
||||
if is_dnd_grab {
|
||||
if let Some((output, pos_within_output)) = self.niri.output_under(new_pos) {
|
||||
let output = output.clone();
|
||||
@@ -2682,10 +2718,9 @@ impl State {
|
||||
self.niri.tablet_cursor_location = None;
|
||||
|
||||
// Inform the layout of an ongoing DnD operation.
|
||||
let mut is_dnd_grab = false;
|
||||
pointer.with_grab(|_, grab| {
|
||||
is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
});
|
||||
let is_dnd_grab = pointer
|
||||
.with_grab(|_, grab| Self::is_dnd_grab(grab.as_any()))
|
||||
.unwrap_or(false);
|
||||
if is_dnd_grab {
|
||||
if let Some((output, pos_within_output)) = self.niri.output_under(pos) {
|
||||
let output = output.clone();
|
||||
@@ -2857,8 +2892,22 @@ impl State {
|
||||
location,
|
||||
};
|
||||
let start_data = PointerOrTouchStartData::Pointer(start_data);
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), false) {
|
||||
let icon = CursorIcon::Grabbing;
|
||||
if let Some(grab) =
|
||||
MoveGrab::new(self, start_data, window.clone(), false, Some(icon))
|
||||
{
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
|
||||
// Set the cursor to Grabbing right away for Mod+LMB since it doesn't
|
||||
// do any other gesture.
|
||||
//
|
||||
// In the overview, we click to activate window and close the overview,
|
||||
// in this case setting the cursor right away would be distracting.
|
||||
if !is_overview_open {
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(icon));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3035,7 +3084,7 @@ impl State {
|
||||
pointer
|
||||
.current_focus()
|
||||
.map(|surface| self.niri.find_root_shell_surface(&surface))
|
||||
.map_or(true, |root| {
|
||||
.is_none_or(|root| {
|
||||
!self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
@@ -3212,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);
|
||||
|
||||
@@ -3986,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();
|
||||
@@ -4106,7 +4156,8 @@ impl State {
|
||||
location: pos,
|
||||
};
|
||||
let start_data = PointerOrTouchStartData::Touch(start_data);
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true) {
|
||||
if let Some(grab) = MoveGrab::new(self, start_data, window.clone(), true, None)
|
||||
{
|
||||
handle.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
@@ -4197,10 +4248,9 @@ impl State {
|
||||
);
|
||||
|
||||
// Inform the layout of an ongoing DnD operation.
|
||||
let mut is_dnd_grab = false;
|
||||
handle.with_grab(|_, grab| {
|
||||
is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
});
|
||||
let is_dnd_grab = handle
|
||||
.with_grab(|_, grab| Self::is_dnd_grab(grab.as_any()))
|
||||
.unwrap_or(false);
|
||||
if is_dnd_grab {
|
||||
if let Some((output, pos_within_output)) = self.niri.output_under(pos) {
|
||||
let output = output.clone();
|
||||
@@ -4241,6 +4291,19 @@ impl State {
|
||||
self.do_action(action, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dnd_grab(grab: &dyn Any) -> bool {
|
||||
// Normal DnD
|
||||
grab.is::<DnDGrab<Self, WlDataSource, WlSurface>>()
|
||||
// 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
|
||||
@@ -4623,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);
|
||||
|
||||
+75
-30
@@ -14,10 +14,11 @@ use smithay::input::touch::{
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial, SERIAL_COUNTER};
|
||||
|
||||
use crate::input::PointerOrTouchStartData;
|
||||
use crate::niri::State;
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerOrTouchStartData<State>,
|
||||
@@ -27,6 +28,12 @@ pub struct MoveGrab {
|
||||
window: Window,
|
||||
gesture: GestureState,
|
||||
enable_view_offset: bool,
|
||||
move_icon: CursorIcon,
|
||||
|
||||
// Accumulated and applied in frame().
|
||||
new_location: Point<f64, Logical>,
|
||||
event_timestamp: Option<Duration>,
|
||||
relative_delta: Option<Point<f64, Logical>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -42,17 +49,24 @@ impl MoveGrab {
|
||||
start_data: PointerOrTouchStartData<State>,
|
||||
window: Window,
|
||||
enable_view_offset: bool,
|
||||
move_icon: Option<CursorIcon>,
|
||||
) -> Option<Self> {
|
||||
let (output, pos_within_output) = state.niri.output_under(start_data.location())?;
|
||||
let location = start_data.location();
|
||||
let (output, pos_within_output) = state.niri.output_under(location)?;
|
||||
|
||||
Some(Self {
|
||||
last_location: start_data.location(),
|
||||
last_location: location,
|
||||
start_data,
|
||||
start_output: output.clone(),
|
||||
start_pos_within_output: pos_within_output,
|
||||
window,
|
||||
gesture: GestureState::Recognizing,
|
||||
enable_view_offset,
|
||||
// Moving windows by their titlebars uses the default cursor by default.
|
||||
move_icon: move_icon.unwrap_or(CursorIcon::Default),
|
||||
new_location: location,
|
||||
event_timestamp: None,
|
||||
relative_delta: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,6 +74,10 @@ impl MoveGrab {
|
||||
self.gesture == GestureState::Move
|
||||
}
|
||||
|
||||
pub fn view_offset_output(&self) -> Option<&Output> {
|
||||
(self.gesture == GestureState::ViewOffset).then_some(&self.start_output)
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, data: &mut State) {
|
||||
let layout = &mut data.niri.layout;
|
||||
match self.gesture {
|
||||
@@ -112,7 +130,7 @@ impl MoveGrab {
|
||||
if self.start_data.is_pointer() {
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
.set_cursor_image(CursorImageStatus::Named(self.move_icon));
|
||||
}
|
||||
|
||||
true
|
||||
@@ -120,19 +138,25 @@ impl MoveGrab {
|
||||
|
||||
fn begin_view_offset(&mut self, data: &mut State) -> bool {
|
||||
let layout = &mut data.niri.layout;
|
||||
let Some((output, ws_idx)) = layout.workspaces().find_map(|(mon, ws_idx, ws)| {
|
||||
let Some(ws_idx) = layout.workspaces().find_map(|(mon, ws_idx, ws)| {
|
||||
let ws_idx = ws
|
||||
.windows()
|
||||
.any(|w| w.window == self.window)
|
||||
.then_some(ws_idx)?;
|
||||
let output = mon?.output().clone();
|
||||
Some((output, ws_idx))
|
||||
let output = mon?.output();
|
||||
|
||||
// If the window moved to a different output, don't start the gesture.
|
||||
if *output != self.start_output {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ws_idx)
|
||||
}) else {
|
||||
// Can no longer start the gesture.
|
||||
return false;
|
||||
};
|
||||
|
||||
layout.view_offset_gesture_begin(&output, Some(ws_idx), false);
|
||||
layout.view_offset_gesture_begin(&self.start_output, Some(ws_idx), false);
|
||||
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
|
||||
@@ -145,14 +169,14 @@ impl MoveGrab {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
location: Point<f64, Logical>,
|
||||
timestamp: Duration,
|
||||
) -> bool {
|
||||
let mut delta = location - self.last_location;
|
||||
self.last_location = location;
|
||||
fn on_frame(&mut self, data: &mut State) -> bool {
|
||||
let Some(timestamp) = self.event_timestamp.take() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let mut delta = self.new_location - self.last_location;
|
||||
let mut relative_delta = self.relative_delta.take().unwrap_or(delta);
|
||||
self.last_location = self.new_location;
|
||||
|
||||
// Try to recognize the gesture.
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
@@ -162,7 +186,7 @@ impl MoveGrab {
|
||||
}
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
let c = location - self.start_data.location();
|
||||
let c = self.new_location - self.start_data.location();
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
let is_floating = data
|
||||
.niri
|
||||
@@ -189,6 +213,7 @@ impl MoveGrab {
|
||||
|
||||
// Apply the whole delta that accumulated during recognizing.
|
||||
delta = c;
|
||||
relative_delta = c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +226,8 @@ impl MoveGrab {
|
||||
};
|
||||
let output = output.clone();
|
||||
|
||||
// Interactive move always uses absolute delta since the window must remain pinned
|
||||
// to the cursor even when it's clamped to monitor bounds.
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
delta,
|
||||
@@ -214,10 +241,11 @@ impl MoveGrab {
|
||||
}
|
||||
}
|
||||
GestureState::ViewOffset => {
|
||||
let res = data
|
||||
.niri
|
||||
.layout
|
||||
.view_offset_gesture_update(-delta.x, timestamp, false);
|
||||
let res = data.niri.layout.view_offset_gesture_update(
|
||||
-relative_delta.x,
|
||||
timestamp,
|
||||
false,
|
||||
);
|
||||
if let Some(output) = res {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
@@ -277,10 +305,11 @@ impl PointerGrab<State> for MoveGrab {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if !self.on_motion(data, event.location, timestamp) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
self.new_location = event.location;
|
||||
|
||||
// Relative motion takes precedence over normal motion.
|
||||
if self.relative_delta.is_none() {
|
||||
self.event_timestamp = Some(Duration::from_millis(u64::from(event.time)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +322,9 @@ impl PointerGrab<State> for MoveGrab {
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
|
||||
*self.relative_delta.get_or_insert_default() += event.delta;
|
||||
self.event_timestamp = Some(Duration::from_micros(event.utime));
|
||||
}
|
||||
|
||||
fn button(
|
||||
@@ -337,6 +369,17 @@ impl PointerGrab<State> for MoveGrab {
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
|
||||
if !self.on_frame(data) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(
|
||||
self,
|
||||
data,
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
get_monotonic_time().as_millis() as u32,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
@@ -468,15 +511,17 @@ impl TouchGrab<State> for MoveGrab {
|
||||
return;
|
||||
}
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if !self.on_motion(data, event.location, timestamp) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
self.new_location = event.location;
|
||||
self.event_timestamp = Some(Duration::from_millis(u64::from(event.time)));
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
|
||||
if !self.on_frame(data) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
|
||||
@@ -2,6 +2,7 @@ use niri_ipc::PickedColor;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::ExportMem as _;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
@@ -12,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_to_vec, RenderTarget};
|
||||
use crate::render_helpers::{render_and_download, RenderCtx, RenderTarget};
|
||||
|
||||
pub struct PickColorGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
@@ -48,15 +49,15 @@ 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 pixels = match render_to_vec(
|
||||
let mapping = match render_and_download(
|
||||
renderer,
|
||||
size,
|
||||
scale,
|
||||
@@ -67,6 +68,10 @@ impl PickColorGrab {
|
||||
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||
}),
|
||||
) {
|
||||
Ok(mapping) => mapping,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let pixels = match renderer.map_texture(&mapping) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
@@ -8,10 +8,11 @@ use smithay::input::pointer::{
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point};
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
@@ -19,9 +20,14 @@ pub struct SpatialMovementGrab {
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
gesture: GestureState,
|
||||
|
||||
// Accumulated and applied in frame().
|
||||
new_location: Point<f64, Logical>,
|
||||
event_timestamp: Option<Duration>,
|
||||
relative_delta: Option<Point<f64, Logical>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
ViewOffset,
|
||||
@@ -35,6 +41,7 @@ impl SpatialMovementGrab {
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let location = start_data.location;
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
@@ -42,52 +49,40 @@ impl SpatialMovementGrab {
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
last_location: location,
|
||||
start_data,
|
||||
output,
|
||||
workspace_id,
|
||||
gesture,
|
||||
new_location: location,
|
||||
event_timestamp: None,
|
||||
relative_delta: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||
pub fn view_offset_output(&self) -> Option<&Output> {
|
||||
(self.gesture == GestureState::ViewOffset).then_some(&self.output)
|
||||
}
|
||||
|
||||
pub fn workspace_switch_output(&self) -> Option<&Output> {
|
||||
(self.gesture == GestureState::WorkspaceSwitch).then_some(&self.output)
|
||||
}
|
||||
|
||||
fn on_frame(&mut self, data: &mut State) -> bool {
|
||||
let Some(timestamp) = self.event_timestamp.take() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for SpatialMovementGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
let delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let delta = self
|
||||
.relative_delta
|
||||
.take()
|
||||
.unwrap_or(self.new_location - self.last_location);
|
||||
self.last_location = self.new_location;
|
||||
|
||||
let layout = &mut data.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => {
|
||||
let c = event.location - self.start_data.location;
|
||||
let c = self.new_location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide. Threshold copied from GTK 4.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
@@ -124,9 +119,47 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
if let Some(output) = output {
|
||||
data.niri.queue_redraw(&output);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
let layout = &mut state.niri.layout;
|
||||
let res = match self.gesture {
|
||||
GestureState::Recognizing => None,
|
||||
GestureState::ViewOffset => layout.view_offset_gesture_end(Some(false)),
|
||||
GestureState::WorkspaceSwitch => layout.workspace_switch_gesture_end(Some(false)),
|
||||
};
|
||||
|
||||
if let Some(output) = res {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for SpatialMovementGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
self.new_location = event.location;
|
||||
|
||||
// Relative motion takes precedence over normal motion.
|
||||
if self.relative_delta.is_none() {
|
||||
self.event_timestamp = Some(Duration::from_millis(u64::from(event.time)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +172,9 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
|
||||
*self.relative_delta.get_or_insert_default() += event.delta;
|
||||
self.event_timestamp = Some(Duration::from_micros(event.utime));
|
||||
}
|
||||
|
||||
fn button(
|
||||
@@ -166,6 +202,17 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
|
||||
if !self.on_frame(data) {
|
||||
// The gesture is no longer ongoing.
|
||||
handle.unset_grab(
|
||||
self,
|
||||
data,
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
get_monotonic_time().as_millis() as u32,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
|
||||
+71
-3
@@ -7,8 +7,8 @@ use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
Action, Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview,
|
||||
Request, Response, Transform, Window, WindowLayout,
|
||||
Action, Cast, CastKind, CastTarget, Event, KeyboardLayouts, LogicalOutput, Mode, Output,
|
||||
OutputConfigChanged, Overview, Request, Response, Transform, Window, WindowLayout,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
Msg::OverviewState => Request::OverviewState,
|
||||
Msg::Casts => Request::Casts,
|
||||
};
|
||||
|
||||
let mut socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
@@ -192,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);
|
||||
@@ -496,6 +497,15 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
let description = parts.join(" and ");
|
||||
println!("Screenshot captured: {description}");
|
||||
}
|
||||
Event::CastsChanged { casts } => {
|
||||
println!("Casts changed: {casts:?}");
|
||||
}
|
||||
Event::CastStartedOrChanged { cast } => {
|
||||
println!("Cast started or changed: {cast:?}");
|
||||
}
|
||||
Event::CastStopped { stream_id } => {
|
||||
println!("Cast stopped: stream id {stream_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,6 +528,28 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("Overview is closed.");
|
||||
}
|
||||
}
|
||||
Msg::Casts => {
|
||||
let Response::Casts(mut casts) = response else {
|
||||
bail!("unexpected response: expected Casts, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let casts = serde_json::to_string(&casts).context("error formatting response")?;
|
||||
println!("{casts}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if casts.is_empty() {
|
||||
println!("No screencasts.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
casts.sort_by_key(|c| (c.session_id, c.stream_id));
|
||||
for cast in casts {
|
||||
print_cast(&cast);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -706,6 +738,42 @@ fn print_window(window: &Window) {
|
||||
);
|
||||
}
|
||||
|
||||
fn print_cast(cast: &Cast) {
|
||||
let active = if cast.is_active { "" } else { " (inactive)" };
|
||||
println!("Cast stream ID {}:{active}", cast.stream_id);
|
||||
println!(" Session ID: {}", cast.session_id);
|
||||
|
||||
let kind = match cast.kind {
|
||||
CastKind::PipeWire => "PipeWire",
|
||||
CastKind::WlrScreencopy => "wlr-screencopy",
|
||||
};
|
||||
println!(" Kind: {kind}");
|
||||
|
||||
match &cast.target {
|
||||
CastTarget::Nothing {} => {
|
||||
println!(" Target: nothing (cleared)");
|
||||
}
|
||||
CastTarget::Output { name } => {
|
||||
println!(" Target: output \"{name}\"");
|
||||
}
|
||||
CastTarget::Window { id } => {
|
||||
println!(" Target: window {id}");
|
||||
}
|
||||
}
|
||||
|
||||
if cast.is_dynamic_target {
|
||||
println!(" Dynamic cast target");
|
||||
}
|
||||
|
||||
if let Some(pid) = cast.pid {
|
||||
println!(" PID: {pid}");
|
||||
}
|
||||
|
||||
if let Some(node_id) = cast.pw_node_id {
|
||||
println!(" PipeWire node ID: {node_id}");
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_rounded(x: f64) -> String {
|
||||
let r = x.round();
|
||||
if (r - x).abs() <= 0.005 {
|
||||
|
||||
+129
-1
@@ -450,6 +450,11 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let is_open = state.overview.is_open;
|
||||
Response::OverviewState(Overview { is_open })
|
||||
}
|
||||
Request::Casts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let casts = state.casts.casts.values().cloned().collect();
|
||||
Response::Casts(casts)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
@@ -458,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
|
||||
@@ -469,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(())
|
||||
}
|
||||
|
||||
@@ -793,6 +806,121 @@ impl State {
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_casts(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_casts");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.casts;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
// Check PipeWire screencasts.
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
{
|
||||
// Check pending dynamic casts.
|
||||
for pending in &self.niri.casting.pending_dynamic_casts {
|
||||
let stream_id = pending.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
// Pending dynamic casts don't change any properties, so we only need to check if
|
||||
// it's missing from the state.
|
||||
if !state.casts.contains_key(&stream_id) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: pending.session_id.get(),
|
||||
stream_id,
|
||||
kind: niri_ipc::CastKind::PipeWire,
|
||||
target: niri_ipc::CastTarget::Nothing {},
|
||||
is_dynamic_target: true,
|
||||
is_active: false,
|
||||
pid: None,
|
||||
pw_node_id: None,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
|
||||
// Check active casts.
|
||||
for cast in &self.niri.casting.casts {
|
||||
let stream_id = cast.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
let pw_node_id = cast.node_id();
|
||||
if state.casts.get(&stream_id).is_none_or(|existing| {
|
||||
// Only these properties can change.
|
||||
existing.is_active != cast.is_active()
|
||||
|| !cast.target.matches(&existing.target)
|
||||
|| existing.pw_node_id != pw_node_id
|
||||
}) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: cast.session_id.get(),
|
||||
stream_id,
|
||||
kind: niri_ipc::CastKind::PipeWire,
|
||||
target: cast.target.make_ipc(),
|
||||
is_dynamic_target: cast.dynamic_target,
|
||||
is_active: cast.is_active(),
|
||||
pid: None,
|
||||
pw_node_id,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check screencopy casts.
|
||||
//
|
||||
// First, clear expired casts. Ideally we'd have a deadline timer, but our 1 second frame
|
||||
// callback timer calls refresh regularly, so that's fine as is.
|
||||
self.niri.screencopy_state.clear_expired_casts();
|
||||
|
||||
for queue in self.niri.screencopy_state.queues() {
|
||||
if let Some(cast_info) = queue.cast() {
|
||||
let stream_id = cast_info.stream_id.get();
|
||||
seen.insert(stream_id);
|
||||
|
||||
if state.casts.get(&stream_id).is_none_or(|existing| {
|
||||
// Only this property can change.
|
||||
match &existing.target {
|
||||
niri_ipc::CastTarget::Output { name } => *name != cast_info.output_name,
|
||||
_ => true,
|
||||
}
|
||||
}) {
|
||||
let cast = niri_ipc::Cast {
|
||||
session_id: cast_info.session_id.get(),
|
||||
stream_id,
|
||||
kind: niri_ipc::CastKind::WlrScreencopy,
|
||||
target: niri_ipc::CastTarget::Output {
|
||||
name: cast_info.output_name.clone(),
|
||||
},
|
||||
is_dynamic_target: false,
|
||||
is_active: true,
|
||||
pid: queue.credentials().map(|creds| creds.pid),
|
||||
pw_node_id: None,
|
||||
};
|
||||
events.push(Event::CastStartedOrChanged { cast });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stopped casts.
|
||||
for stream_id in state.casts.keys() {
|
||||
if !seen.contains(stream_id) {
|
||||
events.push(Event::CastStopped {
|
||||
stream_id: *stream_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ipc_config_loaded(&mut self, failed: bool) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
|
||||
+150
-35
@@ -1,21 +1,23 @@
|
||||
use niri_config::utils::MergeWith as _;
|
||||
use niri_config::{Config, LayerRule};
|
||||
use smithay::backend::renderer::element::surface::{
|
||||
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||
};
|
||||
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::{RenderTarget, SplitElements};
|
||||
use crate::render_helpers::surface::push_elements_from_surface_tree;
|
||||
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)]
|
||||
@@ -23,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>,
|
||||
|
||||
@@ -47,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,
|
||||
@@ -66,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,
|
||||
}
|
||||
}
|
||||
@@ -81,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) {
|
||||
@@ -129,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;
|
||||
@@ -156,19 +184,25 @@ impl MappedLayer {
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
pub fn render_normal<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut ctx: RenderCtx<R>,
|
||||
ns: Option<usize>,
|
||||
location: Point<f64, Logical>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
let mut rv = SplitElements::default();
|
||||
|
||||
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);
|
||||
|
||||
@@ -179,40 +213,121 @@ impl MappedLayer {
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = location;
|
||||
|
||||
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;
|
||||
|
||||
rv.popups.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::ScanoutCandidate,
|
||||
));
|
||||
}
|
||||
|
||||
rv.normal = render_elements_from_surface_tree(
|
||||
renderer,
|
||||
push_elements_from_surface_tree(
|
||||
ctx.renderer,
|
||||
surface,
|
||||
buf_pos.to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::ScanoutCandidate,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
rv.normal
|
||||
.extend(self.shadow.render(renderer, location).map(Into::into));
|
||||
self.shadow
|
||||
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
|
||||
|
||||
rv
|
||||
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,
|
||||
mut ctx: RenderCtx<R>,
|
||||
ns: Option<usize>,
|
||||
location: Point<f64, Logical>,
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
|
||||
) {
|
||||
if ctx.target.should_block_out(self.rules.block_out_from) {
|
||||
return;
|
||||
}
|
||||
|
||||
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, 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(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+28
-23
@@ -1,13 +1,14 @@
|
||||
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;
|
||||
|
||||
/// Rules fully resolved for a layer-shell surface.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct ResolvedLayerRules {
|
||||
/// Extra opacity to draw this layer surface with.
|
||||
pub opacity: Option<f32>,
|
||||
@@ -26,33 +27,19 @@ 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 {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
opacity: None,
|
||||
block_out_from: None,
|
||||
shadow: ShadowRule {
|
||||
off: false,
|
||||
on: false,
|
||||
offset: None,
|
||||
softness: None,
|
||||
spread: None,
|
||||
draw_behind_window: None,
|
||||
color: None,
|
||||
inactive_color: None,
|
||||
},
|
||||
geometry_corner_radius: None,
|
||||
place_within_backdrop: false,
|
||||
baba_is_float: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute(rules: &[LayerRule], surface: &LayerSurface, is_at_startup: bool) -> Self {
|
||||
let _span = tracy_client::span!("ResolvedLayerRules::compute");
|
||||
|
||||
let mut resolved = ResolvedLayerRules::empty();
|
||||
let mut resolved = ResolvedLayerRules::default();
|
||||
|
||||
for rule in rules {
|
||||
let matches = |m: &Match| {
|
||||
@@ -90,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
|
||||
@@ -103,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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -20,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)]
|
||||
@@ -28,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>,
|
||||
|
||||
@@ -43,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>,
|
||||
|
||||
@@ -120,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(),
|
||||
@@ -158,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)
|
||||
};
|
||||
@@ -199,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);
|
||||
|
||||
@@ -229,14 +256,14 @@ impl ClosingWindow {
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
vec![
|
||||
Rc::new([
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
]),
|
||||
HashMap::from([(String::from("niri_tex"), buffer.texture().clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
|
||||
+14
-15
@@ -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;
|
||||
@@ -1053,23 +1055,22 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
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,
|
||||
) -> Vec<FloatingSpaceRenderElement<R>> {
|
||||
let mut rv = Vec::new();
|
||||
|
||||
push: &mut dyn FnMut(FloatingSpaceRenderElement<R>),
|
||||
) {
|
||||
let scale = Scale::from(self.scale);
|
||||
|
||||
// Draw the closing windows on top of the other windows.
|
||||
//
|
||||
// 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);
|
||||
rv.push(elem.into());
|
||||
let elem = closing.render(ctx.as_gles(), view_rect, scale);
|
||||
push(elem.into());
|
||||
}
|
||||
|
||||
let active = self.active_window_id.clone();
|
||||
@@ -1077,13 +1078,11 @@ 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();
|
||||
|
||||
rv.extend(
|
||||
tile.render(renderer, tile_pos, focus_ring, target)
|
||||
.map(Into::into),
|
||||
);
|
||||
let xray_pos = xray_pos.offset(tile_pos);
|
||||
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
@@ -220,18 +219,17 @@ impl FocusRing {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
let mut rv = ArrayVec::<_, 8>::new();
|
||||
|
||||
push: &mut dyn FnMut(FocusRingRenderElement),
|
||||
) {
|
||||
if self.config.off {
|
||||
return rv.into_iter();
|
||||
return;
|
||||
}
|
||||
|
||||
let border_width = -self.locations[0].y;
|
||||
|
||||
// If drawing as a border with width = 0, then there's nothing to draw.
|
||||
if self.is_border && border_width == 0. {
|
||||
return rv.into_iter();
|
||||
return;
|
||||
}
|
||||
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
@@ -244,7 +242,7 @@ impl FocusRing {
|
||||
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
|
||||
.into()
|
||||
};
|
||||
rv.push(elem);
|
||||
push(elem);
|
||||
};
|
||||
|
||||
if self.is_border {
|
||||
@@ -258,8 +256,6 @@ impl FocusRing {
|
||||
location + self.locations[0],
|
||||
);
|
||||
}
|
||||
|
||||
rv.into_iter()
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f64 {
|
||||
|
||||
@@ -59,7 +59,8 @@ impl InsertHintElement {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
push: &mut dyn FnMut(FocusRingRenderElement),
|
||||
) {
|
||||
self.inner.render(renderer, location, push)
|
||||
}
|
||||
}
|
||||
|
||||
+155
-58
@@ -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, SplitElements};
|
||||
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,35 +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,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>>;
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
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,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
self.render(renderer, location, scale, alpha, target).normal
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
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,
|
||||
) -> Vec<LayoutElementRenderElement<R>> {
|
||||
self.render(renderer, location, scale, alpha, target).popups
|
||||
xray_pos: XrayPos,
|
||||
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
|
||||
) {
|
||||
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.
|
||||
@@ -262,6 +290,9 @@ pub trait LayoutElement {
|
||||
Some(requested)
|
||||
}
|
||||
|
||||
fn is_windowed_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_pending_windowed_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -269,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;
|
||||
@@ -345,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,
|
||||
@@ -498,6 +546,7 @@ pub enum HitType {
|
||||
enum OverviewProgress {
|
||||
Animation(Animation),
|
||||
Gesture(OverviewGesture),
|
||||
Open,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -604,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,
|
||||
@@ -628,6 +678,7 @@ impl OverviewProgress {
|
||||
match self {
|
||||
OverviewProgress::Animation(anim) => anim.value(),
|
||||
OverviewProgress::Gesture(gesture) => gesture.value,
|
||||
OverviewProgress::Open => 1.,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2648,9 +2699,11 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.overview_open {
|
||||
if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
|
||||
if anim.is_done() {
|
||||
if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
|
||||
if anim.is_done() {
|
||||
if self.overview_open {
|
||||
self.overview_progress = Some(OverviewProgress::Open);
|
||||
} else {
|
||||
self.overview_progress = None;
|
||||
}
|
||||
}
|
||||
@@ -2674,19 +2727,19 @@ impl<W: LayoutElement> Layout<W> {
|
||||
pub fn are_animations_ongoing(&self, output: Option<&Output>) -> bool {
|
||||
// Keep advancing animations if we might need to scroll the view.
|
||||
if let Some(dnd) = &self.dnd {
|
||||
if output.map_or(true, |output| *output == dnd.output) {
|
||||
if output.is_none_or(|output| *output == dnd.output) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if output.map_or(true, |output| *output == move_.output) {
|
||||
if output.is_none_or(|output| *output == move_.output) {
|
||||
if move_.tile.are_animations_ongoing() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep advancing animations if we might need to scroll the view.
|
||||
if !move_.is_floating {
|
||||
if !move_.is_floating || self.overview_open {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2720,10 +2773,20 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
|
||||
if output.map_or(true, |output| move_.output == *output) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2736,12 +2799,14 @@ 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;
|
||||
};
|
||||
|
||||
for (idx, mon) in monitors.iter_mut().enumerate() {
|
||||
if output.map_or(true, |output| mon.output == *output) {
|
||||
if output.is_none_or(|output| mon.output == *output) {
|
||||
let is_active = self.is_active
|
||||
&& idx == *active_monitor_idx
|
||||
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
|
||||
@@ -2808,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,
|
||||
@@ -3271,7 +3335,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
let mon = &mut monitors[mon_idx];
|
||||
let activate = activate.map_smart(|| {
|
||||
window.map_or(true, |win| {
|
||||
window.is_none_or(|win| {
|
||||
mon_idx == *active_monitor_idx
|
||||
&& mon.active_window().map(|win| win.id()) == Some(win)
|
||||
})
|
||||
@@ -4586,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;
|
||||
}
|
||||
}
|
||||
@@ -4599,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;
|
||||
}
|
||||
}
|
||||
@@ -4610,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;
|
||||
}
|
||||
}
|
||||
@@ -4709,38 +4806,38 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_interactive_move_for_output<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
renderer: &mut R,
|
||||
pub fn render_interactive_move_for_output<R: NiriRenderer>(
|
||||
&self,
|
||||
ctx: RenderCtx<R>,
|
||||
output: &Output,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = RescaleRenderElement<TileRenderElement<R>>> + 'a {
|
||||
push: &mut dyn FnMut(RescaleRenderElement<TileRenderElement<R>>),
|
||||
) {
|
||||
if self.update_render_elements_time != self.clock.now() {
|
||||
error!("clock moved between updating render elements and rendering");
|
||||
}
|
||||
|
||||
let mut rv = None;
|
||||
let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if &move_.output == output {
|
||||
let scale = Scale::from(move_.output.current_scale().fractional_scale());
|
||||
let zoom = self.overview_zoom();
|
||||
let location = move_.tile_render_location(zoom);
|
||||
let iter = move_
|
||||
.tile
|
||||
.render(renderer, location, true, target)
|
||||
.map(move |elem| {
|
||||
RescaleRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
zoom,
|
||||
)
|
||||
});
|
||||
rv = Some(iter);
|
||||
}
|
||||
if &move_.output != output {
|
||||
return;
|
||||
}
|
||||
|
||||
rv.into_iter().flatten()
|
||||
let scale = Scale::from(move_.output.current_scale().fractional_scale());
|
||||
let zoom = self.overview_zoom();
|
||||
let pos_in_backdrop = move_.tile_render_location(zoom);
|
||||
let xray_pos = XrayPos::new(pos_in_backdrop, zoom);
|
||||
|
||||
move_
|
||||
.tile
|
||||
.render(ctx, pos_in_backdrop, xray_pos, true, &mut |elem| {
|
||||
push(RescaleRenderElement::from_element(
|
||||
elem,
|
||||
pos_in_backdrop.to_physical_precise_round(scale),
|
||||
zoom,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, is_active: bool) {
|
||||
|
||||
+98
-112
@@ -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::{
|
||||
@@ -282,6 +283,7 @@ impl From<&super::OverviewProgress> for OverviewProgress {
|
||||
match value {
|
||||
super::OverviewProgress::Animation(anim) => Self::Animation(anim.clone()),
|
||||
super::OverviewProgress::Gesture(gesture) => Self::Value(gesture.value),
|
||||
super::OverviewProgress::Open => Self::Value(1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -870,9 +872,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let new_id = self.workspaces[new_idx].id();
|
||||
|
||||
let activate = activate.map_smart(|| {
|
||||
window.map_or(true, |win| {
|
||||
self.active_window().map(|win| win.id()) == Some(win)
|
||||
})
|
||||
window.is_none_or(|win| self.active_window().map(|win| win.id()) == Some(win))
|
||||
});
|
||||
|
||||
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||
@@ -1491,6 +1491,13 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
(0..=self.workspaces.len()).map(move |idx| {
|
||||
let y = first_ws_y + idx as f64 * ws_height_with_gap;
|
||||
let loc = Point::from((0., y)) + static_offset;
|
||||
|
||||
// Even though all components that go into loc are rounded to physical pixels, the
|
||||
// floating point addition may lose precision. This can result for example in the
|
||||
// current workspace having y = 0.0000000000002 and thus missing pointer hits at the
|
||||
// monitor edge with y = 0. So, post-round the location too.
|
||||
let loc = loc.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
Rectangle::new(loc, ws_size)
|
||||
})
|
||||
}
|
||||
@@ -1639,40 +1646,35 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
pub fn render_insert_hint_between_workspaces<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = MonitorRenderElement<R>> {
|
||||
let mut rv = None;
|
||||
|
||||
if !self.options.layout.insert_hint.off {
|
||||
if let Some(render_loc) = self.insert_hint_render_loc {
|
||||
if let InsertWorkspace::NewAt(_) = render_loc.workspace {
|
||||
let iter = self
|
||||
.insert_hint_element
|
||||
.render(renderer, render_loc.location)
|
||||
.map(MonitorInnerRenderElement::UncroppedInsertHint);
|
||||
rv = Some(iter);
|
||||
}
|
||||
}
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
if self.options.layout.insert_hint.off {
|
||||
return;
|
||||
}
|
||||
let Some(render_loc) = self.insert_hint_render_loc else {
|
||||
return;
|
||||
};
|
||||
let InsertWorkspace::NewAt(_) = render_loc.workspace else {
|
||||
return;
|
||||
};
|
||||
|
||||
rv.into_iter().flatten().map(|elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::default(), 1.);
|
||||
RelocateRenderElement::from_element(elem, Point::default(), Relocate::Relative)
|
||||
})
|
||||
self.insert_hint_element
|
||||
.render(renderer, render_loc.location, &mut |elem| {
|
||||
let elem = MonitorInnerRenderElement::UncroppedInsertHint(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::default(), 1.);
|
||||
let elem =
|
||||
RelocateRenderElement::from_element(elem, Point::default(), Relocate::Relative);
|
||||
push(elem);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_elements<'a, R: NiriRenderer>(
|
||||
&'a self,
|
||||
renderer: &'a mut R,
|
||||
target: RenderTarget,
|
||||
pub fn render_workspaces<R: NiriRenderer>(
|
||||
&self,
|
||||
mut ctx: RenderCtx<R>,
|
||||
focus_ring: bool,
|
||||
) -> impl Iterator<
|
||||
Item = (
|
||||
Rectangle<f64, Logical>,
|
||||
MonitorRenderElement<R>,
|
||||
impl Iterator<Item = MonitorRenderElement<R>> + 'a,
|
||||
),
|
||||
> {
|
||||
let _span = tracy_client::span!("Monitor::render_elements");
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Monitor::render_workspaces");
|
||||
|
||||
let scale = self.scale.fractional_scale();
|
||||
// Ceil the height in physical pixels.
|
||||
@@ -1702,95 +1704,79 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
|
||||
// Draw the insert hint.
|
||||
let mut insert_hint = None;
|
||||
if !self.options.layout.insert_hint.off {
|
||||
if let Some(render_loc) = self.insert_hint_render_loc {
|
||||
if let InsertWorkspace::Existing(workspace_id) = render_loc.workspace {
|
||||
insert_hint = Some((
|
||||
workspace_id,
|
||||
self.insert_hint_element
|
||||
.render(renderer, render_loc.location),
|
||||
));
|
||||
let insert_hint_render_loc = self
|
||||
.insert_hint_render_loc
|
||||
.filter(|_| !self.options.layout.insert_hint.off);
|
||||
|
||||
let scale_relocate = move |geo: Rectangle<f64, Logical>, elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_geo() is already
|
||||
// rounded to physical pixels, but it's in the logical coordinate
|
||||
// space, so we need to convert it to physical.
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
};
|
||||
|
||||
for (ws, geo) in self.workspaces_with_render_geo() {
|
||||
// Macro instead of closure because ws and insert hint have different elem types.
|
||||
macro_rules! push {
|
||||
() => {{
|
||||
&mut |elem| {
|
||||
let elem = CropRenderElement::from_element(elem, scale, crop_bounds);
|
||||
if let Some(elem) = elem {
|
||||
let elem = MonitorInnerRenderElement::from(elem);
|
||||
push(scale_relocate(geo, elem));
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
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(ctx.renderer, loc.location, push!());
|
||||
}
|
||||
}
|
||||
|
||||
ws.render_scrolling(ctx.r(), xray_pos, focus_ring, push!());
|
||||
}
|
||||
|
||||
self.workspaces_with_render_geo().map(move |(ws, geo)| {
|
||||
let map_ws_contents = move |elem: WorkspaceRenderElement<R>| {
|
||||
let elem = CropRenderElement::from_element(elem, scale, crop_bounds)?;
|
||||
let elem = MonitorInnerRenderElement::Workspace(elem);
|
||||
Some(elem)
|
||||
};
|
||||
|
||||
let (floating, scrolling) = ws.render_elements(renderer, target, focus_ring);
|
||||
let floating = floating.filter_map(map_ws_contents);
|
||||
let scrolling = scrolling.filter_map(map_ws_contents);
|
||||
|
||||
let hint = if matches!(insert_hint, Some((hint_ws_id, _)) if hint_ws_id == ws.id()) {
|
||||
let iter = insert_hint.take().unwrap().1;
|
||||
let iter = iter.filter_map(move |elem| {
|
||||
let elem = CropRenderElement::from_element(elem, scale, crop_bounds)?;
|
||||
let elem = MonitorInnerRenderElement::InsertHint(elem);
|
||||
Some(elem)
|
||||
});
|
||||
Some(iter)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hint = hint.into_iter().flatten();
|
||||
|
||||
let iter = floating.chain(hint).chain(scrolling);
|
||||
|
||||
let scale_relocate = move |elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_positions() is already
|
||||
// rounded to physical pixels, but it's in the logical coordinate
|
||||
// space, so we need to convert it to physical.
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
};
|
||||
|
||||
let iter = iter.map(scale_relocate);
|
||||
|
||||
let background = ws.render_background();
|
||||
let background = scale_relocate(MonitorInnerRenderElement::SolidColor(background));
|
||||
|
||||
(geo, background, iter)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_workspace_shadows<'a, R: NiriRenderer>(
|
||||
&'a self,
|
||||
renderer: &'a mut R,
|
||||
) -> impl Iterator<Item = MonitorRenderElement<R>> + 'a {
|
||||
pub fn render_workspace_shadows<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
push: &mut dyn FnMut(MonitorRenderElement<R>),
|
||||
) {
|
||||
let Some(progress) = self.overview_progress.as_ref().map(|p| p.clamped_value()) else {
|
||||
return;
|
||||
};
|
||||
let alpha = progress.clamp(0., 1.) as f32;
|
||||
|
||||
let _span = tracy_client::span!("Monitor::render_workspace_shadows");
|
||||
|
||||
let scale = self.scale.fractional_scale();
|
||||
let zoom = self.overview_zoom();
|
||||
let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
|
||||
|
||||
self.workspaces_with_render_geo()
|
||||
.flat_map(move |(ws, geo)| {
|
||||
let shadow = overview_clamped_progress.map(|value| {
|
||||
ws.render_shadow(renderer)
|
||||
.map(move |elem| elem.with_alpha(value.clamp(0., 1.) as f32))
|
||||
.map(MonitorInnerRenderElement::Shadow)
|
||||
});
|
||||
let iter = shadow.into_iter().flatten();
|
||||
|
||||
iter.map(move |elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
})
|
||||
})
|
||||
for (ws, geo) in self.workspaces_with_render_geo() {
|
||||
ws.render_shadow(renderer, &mut |elem| {
|
||||
let elem = elem.with_alpha(alpha);
|
||||
let elem = MonitorInnerRenderElement::Shadow(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
geo.loc.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
push(elem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -39,8 +40,6 @@ impl OpenAnimation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.anim.is_done()
|
||||
}
|
||||
@@ -104,14 +103,14 @@ impl OpenAnimation {
|
||||
None,
|
||||
scale.x as f32,
|
||||
alpha,
|
||||
vec![
|
||||
Rc::new([
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
mat3_uniform("niri_geo_to_tex", geo_to_tex),
|
||||
Uniform::new("niri_progress", progress as f32),
|
||||
Uniform::new("niri_clamped_progress", clamped_progress as f32),
|
||||
Uniform::new("niri_random_seed", self.random_seed),
|
||||
],
|
||||
]),
|
||||
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
|
||||
+58
-41
@@ -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;
|
||||
@@ -336,8 +337,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for tile in self.tiles_mut() {
|
||||
tile.update_shaders();
|
||||
for col in &mut self.columns {
|
||||
col.update_shaders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -986,6 +987,23 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.data.insert(idx, ColumnData::new(&column));
|
||||
self.columns.insert(idx, column);
|
||||
|
||||
if !was_empty && idx <= self.active_column_idx {
|
||||
self.active_column_idx += 1;
|
||||
}
|
||||
|
||||
// Animate movement of other columns.
|
||||
let offset = self.column_x(idx + 1) - self.column_x(idx);
|
||||
let config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
||||
if self.active_column_idx <= idx {
|
||||
for col in &mut self.columns[idx + 1..] {
|
||||
col.animate_move_from_with_config(-offset, config);
|
||||
}
|
||||
} else {
|
||||
for col in &mut self.columns[..idx] {
|
||||
col.animate_move_from_with_config(offset, config);
|
||||
}
|
||||
}
|
||||
|
||||
if activate {
|
||||
// If this is the first window on an empty workspace, remove the effect of whatever
|
||||
// view_offset was left over and skip the animation.
|
||||
@@ -1002,21 +1020,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0);
|
||||
self.activate_column_with_anim_config(idx, anim_config);
|
||||
self.activate_prev_column_on_removal = prev_offset;
|
||||
} else if !was_empty && idx <= self.active_column_idx {
|
||||
self.active_column_idx += 1;
|
||||
}
|
||||
|
||||
// Animate movement of other columns.
|
||||
let offset = self.column_x(idx + 1) - self.column_x(idx);
|
||||
let config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
||||
if self.active_column_idx <= idx {
|
||||
for col in &mut self.columns[idx + 1..] {
|
||||
col.animate_move_from_with_config(-offset, config);
|
||||
}
|
||||
} else {
|
||||
for col in &mut self.columns[..idx] {
|
||||
col.animate_move_from_with_config(offset, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,11 +1387,6 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
// We might need to move the view to ensure the resized window is still visible. But
|
||||
// only do it when the view isn't frozen by an interactive resize or a view gesture.
|
||||
if self.interactive_resize.is_none() && !self.view_offset.is_gesture() {
|
||||
// Restore the view offset upon unfullscreening if needed.
|
||||
if let Some(prev_offset) = unfullscreen_offset {
|
||||
self.animate_view_offset(col_idx, prev_offset);
|
||||
}
|
||||
|
||||
// Synchronize the horizontal view movement with the resize so that it looks nice.
|
||||
// This is especially important for always-centered view.
|
||||
let config = if ongoing_resize_anim {
|
||||
@@ -1397,6 +1395,11 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.options.animations.horizontal_view_movement.0
|
||||
};
|
||||
|
||||
// Restore the view offset upon unfullscreening if needed.
|
||||
if let Some(prev_offset) = unfullscreen_offset {
|
||||
self.animate_view_offset_with_config(col_idx, prev_offset, config);
|
||||
}
|
||||
|
||||
// FIXME: we will want to skip the animation in some cases here to make continuously
|
||||
// resizing windows not look janky.
|
||||
self.animate_view_offset_to_column_with_config(None, col_idx, None, config);
|
||||
@@ -1856,7 +1859,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
self.activate_prev_column_on_removal = None;
|
||||
}
|
||||
|
||||
if target_column_idx < self.active_column_idx {
|
||||
if target_column_idx <= self.active_column_idx {
|
||||
// Tiles to the left animate from the following column.
|
||||
offset.x += self.column_x(target_column_idx + 1) - self.column_x(target_column_idx);
|
||||
}
|
||||
@@ -2895,25 +2898,24 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
.is_fullscreen()
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
mut ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
) -> Vec<ScrollingSpaceRenderElement<R>> {
|
||||
let mut rv = vec![];
|
||||
|
||||
push: &mut dyn FnMut(ScrollingSpaceRenderElement<R>),
|
||||
) {
|
||||
let scale = Scale::from(self.scale);
|
||||
|
||||
// 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);
|
||||
rv.push(elem.into());
|
||||
let elem = closing.render(ctx.as_gles(), view_rect, scale);
|
||||
push(elem.into());
|
||||
}
|
||||
|
||||
if self.columns.is_empty() {
|
||||
return rv;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut first = true;
|
||||
@@ -2928,7 +2930,8 @@ 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);
|
||||
rv.extend(col.tab_indicator.render(renderer, pos).map(Into::into));
|
||||
col.tab_indicator
|
||||
.render(ctx.renderer, pos, &mut |elem| push(elem.into()));
|
||||
}
|
||||
|
||||
for (tile, tile_off, visible) in col.tiles_in_render_order() {
|
||||
@@ -2953,14 +2956,12 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
continue;
|
||||
}
|
||||
|
||||
rv.extend(
|
||||
tile.render(renderer, tile_pos, focus_ring, target)
|
||||
.map(Into::into),
|
||||
);
|
||||
let xray_pos = xray_pos.offset(tile_pos);
|
||||
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, HitType)> {
|
||||
@@ -3493,7 +3494,15 @@ impl<W: LayoutElement> ScrollingSpace<W> {
|
||||
if gesture.dnd_last_event_time.is_some() && gesture.tracker.pos() == 0. {
|
||||
// DnD didn't scroll anything, so preserve the current view position (rather than
|
||||
// snapping the window).
|
||||
self.view_offset = ViewOffset::Static(gesture.delta_from_tracker);
|
||||
|
||||
// If there's an ongoing animation within the gesture (e.g. from a window being removed
|
||||
// during DnD), preserve it.
|
||||
if let Some(mut anim) = gesture.animation.take() {
|
||||
anim.offset(gesture.current_view_offset);
|
||||
self.view_offset = ViewOffset::Animation(anim);
|
||||
} else {
|
||||
self.view_offset = ViewOffset::Static(gesture.delta_from_tracker);
|
||||
}
|
||||
|
||||
if !self.columns.is_empty() {
|
||||
// Just in case, make sure the active window remains on screen.
|
||||
@@ -4054,6 +4063,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for tile in &mut self.tiles {
|
||||
tile.update_shaders();
|
||||
}
|
||||
|
||||
self.tab_indicator.update_shaders();
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {
|
||||
if let Some(move_) = &mut self.move_animation {
|
||||
if move_.anim.is_done() {
|
||||
|
||||
@@ -166,19 +166,19 @@ impl Shadow {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
push: &mut dyn FnMut(ShadowRenderElement),
|
||||
) {
|
||||
if !self.config.on {
|
||||
return None.into_iter().flatten();
|
||||
return;
|
||||
}
|
||||
|
||||
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
|
||||
if !has_shadow_shader {
|
||||
return None.into_iter().flatten();
|
||||
return;
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_rects)
|
||||
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
for (shader, rect) in zip(&self.shaders, &self.shader_rects) {
|
||||
push(shader.clone().with_location(location + rect.loc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,17 +294,17 @@ impl TabIndicator {
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
pos: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
|
||||
push: &mut dyn FnMut(TabIndicatorRenderElement),
|
||||
) {
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
if !has_border_shader {
|
||||
return None.into_iter().flatten();
|
||||
return;
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_locs)
|
||||
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
|
||||
.map(TabIndicatorRenderElement::from);
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
for (shader, loc) in zip(&self.shaders, &self.shader_locs) {
|
||||
let elem = shader.clone().with_location(pos + *loc);
|
||||
push(TabIndicatorRenderElement::from(elem));
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra size occupied by the tab indicator.
|
||||
|
||||
+69
-11
@@ -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(),
|
||||
}));
|
||||
}
|
||||
@@ -166,17 +168,6 @@ impl LayoutElement for TestWindow {
|
||||
false
|
||||
}
|
||||
|
||||
fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
_location: Point<f64, Logical>,
|
||||
_scale: Scale<f64>,
|
||||
_alpha: f32,
|
||||
_target: RenderTarget,
|
||||
) -> SplitElements<LayoutElementRenderElement<R>> {
|
||||
SplitElements::default()
|
||||
}
|
||||
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
@@ -252,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()
|
||||
}
|
||||
@@ -3674,6 +3669,69 @@ fn tabs_with_different_border() {
|
||||
check_ops_with_options(options, ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expel_pending_left_from_fullscreen_tabbed_column() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::FullscreenWindow(1),
|
||||
Op::Communicate(1),
|
||||
// 1 is now fullscreen, view_offset_to_restore is set.
|
||||
Op::ToggleColumnTabbedDisplay,
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(2),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft { id: Some(2) },
|
||||
// 2 is consumed into a fullscreen column, fullscreen is requested but not applied.
|
||||
//
|
||||
// Now, get it back out while keeping it focused.
|
||||
//
|
||||
// Importantly, we expel it *left*, which results in adding a new column with the exact
|
||||
// same active_column_idx.
|
||||
Op::FocusWindow(2),
|
||||
Op::ConsumeOrExpelWindowLeft { id: None },
|
||||
];
|
||||
|
||||
check_ops(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_render_geo_at_fractional_scale() {
|
||||
let ops = [
|
||||
Op::AddScaledOutput {
|
||||
id: 1,
|
||||
scale: 1.1,
|
||||
layout_config: None,
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::FocusWorkspaceDown,
|
||||
Op::CompleteAnimations,
|
||||
];
|
||||
|
||||
let layout = check_ops(ops);
|
||||
|
||||
let MonitorSet::Normal { monitors, .. } = &layout.monitor_set else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mon = &monitors[0];
|
||||
let mut iter = mon.workspaces_with_render_geo();
|
||||
let (_ws, geo) = iter.next().unwrap();
|
||||
assert!(
|
||||
iter.next().is_none(),
|
||||
"animations are completed, only one workspace should be visible"
|
||||
);
|
||||
assert_eq!(
|
||||
geo.loc.y, 0.,
|
||||
"active workspace must be at y = 0 exactly, \
|
||||
otherwise a pointer against the screen edge at y = 0 won't hit it"
|
||||
);
|
||||
}
|
||||
|
||||
fn parent_id_causes_loop(layout: &Layout<TestWindow>, id: usize, mut parent_id: usize) -> bool {
|
||||
if parent_id == id {
|
||||
return true;
|
||||
|
||||
@@ -338,6 +338,65 @@ fn interactive_move_unfullscreen_to_floating_stops_dnd_scroll() {
|
||||
check_ops(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_move_restore_to_floating_animates_view_offset() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(1),
|
||||
},
|
||||
Op::AddWindow {
|
||||
params: TestWindowParams::new(2),
|
||||
},
|
||||
// Toggle window 1 to floating.
|
||||
Op::FocusWindow(1),
|
||||
Op::ToggleWindowFloating { id: None },
|
||||
// Fullscreen window 1 - it moves to scrolling with restore_to_floating = true.
|
||||
Op::FullscreenWindow(1),
|
||||
Op::Communicate(1),
|
||||
Op::CompleteAnimations,
|
||||
];
|
||||
|
||||
let mut layout = check_ops(ops);
|
||||
|
||||
// Verify window 1 is in scrolling and has restore_to_floating = true.
|
||||
let scrolling = layout.active_workspace().unwrap().scrolling();
|
||||
let tile1 = scrolling.tiles().find(|t| *t.window().id() == 1).unwrap();
|
||||
assert!(
|
||||
tile1.restore_to_floating,
|
||||
"window 1 should have restore_to_floating = true"
|
||||
);
|
||||
|
||||
let ops = [
|
||||
// Start interactive move on window 1.
|
||||
Op::InteractiveMoveBegin {
|
||||
window: 1,
|
||||
output_idx: 1,
|
||||
px: 100.,
|
||||
py: 100.,
|
||||
},
|
||||
// Update with a large delta to trigger the unmaximize.
|
||||
Op::InteractiveMoveUpdate {
|
||||
window: 1,
|
||||
dx: 1000.,
|
||||
dy: 1000.,
|
||||
output_idx: 1,
|
||||
px: 0.,
|
||||
py: 0.,
|
||||
},
|
||||
];
|
||||
check_ops_on_layout(&mut layout, ops);
|
||||
|
||||
// Window 1 should now be removed from the workspace (in the interactive move state).
|
||||
// Window 2 should be the only window in the scrolling space.
|
||||
let scrolling = layout.active_workspace().unwrap().scrolling();
|
||||
assert_eq!(scrolling.tiles().count(), 1);
|
||||
assert!(scrolling.tiles().next().unwrap().window().id() == &2);
|
||||
|
||||
// The view offset should be animating to show window 2.
|
||||
assert!(scrolling.view_offset().is_animation_ongoing());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfullscreen_view_offset_not_reset_during_dnd_gesture() {
|
||||
let ops = [
|
||||
|
||||
+281
-150
@@ -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(
|
||||
@@ -1007,13 +1021,14 @@ impl<W: LayoutElement> Tile<W> {
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
fn render_inner<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
renderer: &mut R,
|
||||
fn render_inner<R: NiriRenderer>(
|
||||
&self,
|
||||
mut ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
mut xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
push: &mut dyn FnMut(TileRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Tile::render_inner");
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
@@ -1038,65 +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().to_f64();
|
||||
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(
|
||||
ctx.r(),
|
||||
window_render_loc,
|
||||
scale,
|
||||
win_alpha,
|
||||
xray_pos,
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
|
||||
// If we're resizing, try to render a shader, or a fallback.
|
||||
let mut resize_shader = None;
|
||||
let mut resize_popups = None;
|
||||
let mut resize_fallback = None;
|
||||
|
||||
let mut pushed_resize = false;
|
||||
if let Some(resize) = &self.resize_animation {
|
||||
resize_popups = Some(
|
||||
self.window
|
||||
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
);
|
||||
if ResizeRenderElement::has_shader(ctx.renderer) {
|
||||
let mut ctx = ctx.as_gles();
|
||||
|
||||
if ResizeRenderElement::has_shader(renderer) {
|
||||
let gles_renderer = renderer.as_gles_renderer();
|
||||
|
||||
if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
|
||||
let window_elements = self.window.render_normal(
|
||||
gles_renderer,
|
||||
if let Some(texture_from) = resize.snapshot.texture(ctx.r(), scale) {
|
||||
let mut window_elements = Vec::new();
|
||||
self.window.render_normal(
|
||||
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();
|
||||
@@ -1125,46 +1144,33 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// This is not a problem for split popups as the code will look for them by
|
||||
// original id when it doesn't find them on the offscreen.
|
||||
self.window.set_offscreen_data(Some(data));
|
||||
resize_shader = Some(elem.into());
|
||||
push(elem.into());
|
||||
pushed_resize = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resize_shader.is_none() {
|
||||
if !pushed_resize {
|
||||
let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]);
|
||||
resize_fallback = Some(
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
area.loc,
|
||||
win_alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
area.loc,
|
||||
win_alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
push(elem.into());
|
||||
pushed_resize = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not resizing, render the window itself.
|
||||
let mut window_surface = None;
|
||||
let mut window_popups = None;
|
||||
let mut rounded_corner_damage = None;
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
if resize_shader.is_none() && resize_fallback.is_none() {
|
||||
let window = self
|
||||
.window
|
||||
.render(renderer, window_render_loc, scale, win_alpha, target);
|
||||
|
||||
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();
|
||||
|
||||
if clip_to_geometry && clip_shader.is_some() {
|
||||
let damage = self.rounded_corner_damage.element();
|
||||
rounded_corner_damage = Some(damage.with_location(window_render_loc).into());
|
||||
}
|
||||
|
||||
window_surface = Some(window.normal.into_iter().map(move |elem| match elem {
|
||||
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.
|
||||
if clip_to_geometry {
|
||||
@@ -1213,37 +1219,41 @@ 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()
|
||||
}
|
||||
};
|
||||
|
||||
window_popups = Some(window.popups.into_iter().map(Into::into));
|
||||
if clip_to_geometry && clip_shader.is_some() {
|
||||
let damage = self.rounded_corner_damage.render(geo);
|
||||
push(damage.into());
|
||||
}
|
||||
|
||||
self.window
|
||||
.render_normal(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| {
|
||||
push(clip(elem))
|
||||
});
|
||||
}
|
||||
|
||||
let rv = resize_popups
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.chain(resize_shader)
|
||||
.chain(resize_fallback)
|
||||
.chain(window_popups.into_iter().flatten())
|
||||
.chain(rounded_corner_damage)
|
||||
.chain(window_surface.into_iter().flatten());
|
||||
|
||||
let elem = (fullscreen_progress > 0.).then(|| {
|
||||
if fullscreen_progress > 0. {
|
||||
let alpha = fullscreen_progress as f32;
|
||||
|
||||
// During the un/fullscreen animation, render a border element in order to use the
|
||||
// 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();
|
||||
let color = self.fullscreen_backdrop.color();
|
||||
BorderRenderElement::new(
|
||||
let elem = BorderRenderElement::new(
|
||||
size,
|
||||
Rectangle::from_size(size),
|
||||
GradientInterpolation::default(),
|
||||
@@ -1256,47 +1266,62 @@ impl<W: LayoutElement> Tile<W> {
|
||||
scale.x as f32,
|
||||
alpha,
|
||||
)
|
||||
.with_location(location)
|
||||
.into()
|
||||
.with_location(location);
|
||||
push(elem.into());
|
||||
} else {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&self.fullscreen_backdrop,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
);
|
||||
push(elem.into());
|
||||
}
|
||||
});
|
||||
let rv = rv.chain(elem);
|
||||
}
|
||||
|
||||
let elem = self.visual_border_width().map(|width| {
|
||||
self.border
|
||||
.render(renderer, location + Point::from((width, width)))
|
||||
.map(Into::into)
|
||||
});
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
if let Some(width) = self.visual_border_width() {
|
||||
self.border.render(
|
||||
ctx.renderer,
|
||||
location + Point::from((width, width)),
|
||||
&mut |elem| push(elem.into()),
|
||||
);
|
||||
}
|
||||
|
||||
// Hide the focus ring when maximized/fullscreened. It's not normally visible anyway due to
|
||||
// being outside the monitor or obscured by a solid colored bar, but it is visible under
|
||||
// semitransparent bars in maximized state (which is a bit weird) and in the overview (also
|
||||
// a bit weird).
|
||||
let elem = (focus_ring && expanded_progress < 1.)
|
||||
.then(|| self.focus_ring.render(renderer, location).map(Into::into));
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
if focus_ring && expanded_progress < 1. {
|
||||
self.focus_ring
|
||||
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
|
||||
}
|
||||
|
||||
let elem = (expanded_progress < 1.)
|
||||
.then(|| self.shadow.render(renderer, location).map(Into::into));
|
||||
rv.chain(elem.into_iter().flatten())
|
||||
if expanded_progress < 1. {
|
||||
self.shadow
|
||||
.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<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
renderer: &mut R,
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
mut ctx: RenderCtx<R>,
|
||||
location: Point<f64, Logical>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
push: &mut dyn FnMut(TileRenderElement<R>),
|
||||
) {
|
||||
let _span = tracy_client::span!("Tile::render");
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
@@ -1306,18 +1331,21 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.as_ref()
|
||||
.map_or(1., |alpha| alpha.anim.clamped_value()) as f32;
|
||||
|
||||
let mut open_anim_elem = None;
|
||||
let mut alpha_anim_elem = None;
|
||||
let mut window_elems = None;
|
||||
|
||||
let mut pushed = false;
|
||||
self.window().set_offscreen_data(None);
|
||||
|
||||
if let Some(open) = &self.open_animation {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
let mut ctx = ctx.as_gles();
|
||||
let mut elements = Vec::new();
|
||||
self.render_inner(
|
||||
ctx.r(),
|
||||
Point::new(0., 0.),
|
||||
xray_pos,
|
||||
focus_ring,
|
||||
&mut |elem| elements.push(elem),
|
||||
);
|
||||
match open.render(
|
||||
renderer,
|
||||
ctx.renderer,
|
||||
&elements,
|
||||
self.animated_tile_size(),
|
||||
location,
|
||||
@@ -1326,23 +1354,31 @@ impl<W: LayoutElement> Tile<W> {
|
||||
) {
|
||||
Ok((elem, data)) => {
|
||||
self.window().set_offscreen_data(Some(data));
|
||||
open_anim_elem = Some(elem.into());
|
||||
push(elem.into());
|
||||
pushed = true;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error rendering window opening animation: {err:?}");
|
||||
}
|
||||
}
|
||||
} else if let Some(alpha) = &self.alpha_animation {
|
||||
let renderer = renderer.as_gles_renderer();
|
||||
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
match alpha.offscreen.render(renderer, scale, &elements) {
|
||||
let mut ctx = ctx.as_gles();
|
||||
let mut elements = Vec::new();
|
||||
self.render_inner(
|
||||
ctx.r(),
|
||||
Point::new(0., 0.),
|
||||
xray_pos,
|
||||
focus_ring,
|
||||
&mut |elem| elements.push(elem),
|
||||
);
|
||||
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);
|
||||
|
||||
self.window().set_offscreen_data(Some(data));
|
||||
alpha_anim_elem = Some(elem.into());
|
||||
push(elem.into());
|
||||
pushed = true;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
|
||||
@@ -1350,43 +1386,138 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
|
||||
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
|
||||
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
|
||||
if !pushed {
|
||||
self.render_inner(ctx, location, xray_pos, focus_ring, &mut |elem| push(elem));
|
||||
}
|
||||
|
||||
open_anim_elem
|
||||
.into_iter()
|
||||
.chain(alpha_anim_elem)
|
||||
.chain(window_elems.into_iter().flatten())
|
||||
}
|
||||
|
||||
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 contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
|
||||
|
||||
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
|
||||
let blocked_out_contents = self.render(
|
||||
renderer,
|
||||
let mut contents = Vec::new();
|
||||
self.render(
|
||||
RenderCtx {
|
||||
target: RenderTarget::Output,
|
||||
renderer,
|
||||
xray: xray.as_deref(),
|
||||
},
|
||||
Point::from((0., 0.)),
|
||||
xray_pos,
|
||||
false,
|
||||
RenderTarget::Screencast,
|
||||
&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(
|
||||
RenderCtx {
|
||||
target: RenderTarget::Screencast,
|
||||
renderer,
|
||||
xray: xray.as_deref(),
|
||||
},
|
||||
Point::from((0., 0.)),
|
||||
xray_pos,
|
||||
false,
|
||||
&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.collect(),
|
||||
blocked_out_contents: blocked_out_contents.collect(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
+46
-26
@@ -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::{
|
||||
@@ -1386,7 +1387,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
pub fn toggle_window_floating(&mut self, id: Option<&W::Id>) {
|
||||
let active_id = self.active_window().map(|win| win.id().clone());
|
||||
let target_is_active = id.map_or(true, |id| Some(id) == active_id.as_ref());
|
||||
let target_is_active = id.is_none_or(|id| Some(id) == active_id.as_ref());
|
||||
let Some(id) = id.cloned().or(active_id) else {
|
||||
return;
|
||||
};
|
||||
@@ -1624,39 +1625,45 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
pub fn render_scrolling<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
) -> (
|
||||
impl Iterator<Item = WorkspaceRenderElement<R>>,
|
||||
impl Iterator<Item = WorkspaceRenderElement<R>>,
|
||||
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
|
||||
) {
|
||||
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
|
||||
let scrolling = self
|
||||
.scrolling
|
||||
.render_elements(renderer, target, scrolling_focus_ring);
|
||||
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
|
||||
self.scrolling
|
||||
.render(ctx, xray_pos, scrolling_focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_floating<R: NiriRenderer>(
|
||||
&self,
|
||||
ctx: RenderCtx<R>,
|
||||
xray_pos: XrayPos,
|
||||
focus_ring: bool,
|
||||
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
|
||||
) {
|
||||
if !self.is_floating_visible() {
|
||||
return;
|
||||
}
|
||||
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
let floating_focus_ring = focus_ring && self.floating_is_active();
|
||||
let floating = self.is_floating_visible().then(|| {
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
let floating =
|
||||
self.floating
|
||||
.render_elements(renderer, view_rect, target, floating_focus_ring);
|
||||
floating.into_iter().map(WorkspaceRenderElement::from)
|
||||
});
|
||||
let floating = floating.into_iter().flatten();
|
||||
|
||||
(floating, scrolling)
|
||||
self.floating
|
||||
.render(ctx, xray_pos, view_rect, floating_focus_ring, &mut |elem| {
|
||||
push(elem.into())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render_shadow<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)))
|
||||
push: &mut dyn FnMut(ShadowRenderElement),
|
||||
) {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)), push);
|
||||
}
|
||||
|
||||
pub fn render_background(&self) -> SolidColorRenderElement {
|
||||
@@ -1680,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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-8
@@ -19,17 +19,11 @@ pub mod niri;
|
||||
pub mod protocols;
|
||||
pub mod render_helpers;
|
||||
pub mod rubber_band;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub mod screencasting;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
pub mod window;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub mod dummy_pw_utils;
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub mod pw_utils;
|
||||
|
||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||
pub use dummy_pw_utils as pw_utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user