mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cb2fe4eeb | |||
| cc2549323d | |||
| 43c1592ab7 | |||
| 78fe4a68db | |||
| 4fe2722a28 | |||
| 7da5fc6169 | |||
| 3d4b762bcc | |||
| 74b016202b | |||
| 6ab055a4b9 | |||
| 98bd9b7abb | |||
| f36e1c2ef2 | |||
| 2243615fe9 | |||
| 7884d3bfea | |||
| fdbc485d78 | |||
| 7e253d2687 | |||
| 15ba2ab300 | |||
| 37840a418a | |||
| 4a4c972ffb | |||
| ba933773ab | |||
| f1cca1a6ca | |||
| 763cd564e3 | |||
| 95eafba346 | |||
| df94662435 | |||
| 430b155929 | |||
| c359d24825 | |||
| e8da89a430 | |||
| feae8c15e6 | |||
| b49f7dcb4d | |||
| 60034a57ef | |||
| 2adbf33fb6 | |||
| 28cc84fbd1 | |||
| e10b968eb0 | |||
| 3b1bf34e21 | |||
| bd927b54e0 | |||
| 66d3a3bd82 | |||
| 36489f1daa | |||
| b2c34e7fe9 | |||
| dcc291d701 | |||
| 8d43efe4ac | |||
| 3bb7e60311 | |||
| d639eb0032 | |||
| d91499486e | |||
| f7106f9658 | |||
| 835490c59a | |||
| 5cde00f6c6 | |||
| 7dc015e16b | |||
| 0db48e2f1b | |||
| 7cfecf4b1b | |||
| 3142838e9e | |||
| 4534d37266 | |||
| ec5112d779 | |||
| c709696237 | |||
| b271409509 | |||
| 500dcca9b7 | |||
| 7210045b2a | |||
| ed20822ce9 | |||
| e88dfae46f | |||
| f95d5a82df | |||
| 7f72c358d5 | |||
| 0d4f0f00c0 | |||
| f2663c738c | |||
| c3609efb7a | |||
| fd1f43673c | |||
| 9d10def7e8 | |||
| e251ca7340 | |||
| 9a527cc571 | |||
| 39f52b7585 | |||
| b447b1f4de | |||
| 1a0fab05b6 | |||
| fbb399f01d | |||
| 6a80ec4704 | |||
| e8b158641b | |||
| 27a715aded | |||
| 926e63a5f3 | |||
| e879199880 | |||
| 5b6b6a5fe1 | |||
| e11af089aa | |||
| 5e549e1323 | |||
| 287480b541 | |||
| a022fedd51 | |||
| a4b8e100c0 | |||
| 62576796be | |||
| 31891e6642 | |||
| 392fc27de1 | |||
| 9e560e7e60 | |||
| cee2ec7ab7 | |||
| 8c4ebb00a1 | |||
| f6aa8c1793 | |||
| a5d58d670b | |||
| b4922086ce | |||
| fd3b1f2b6c | |||
| ee0e2c7f1b | |||
| 4f16be9e4d | |||
| 0f30306fe5 | |||
| 1c6037e612 | |||
| fed86fdb5d | |||
| 3e21585861 | |||
| 9f9c4a99af | |||
| b220cdbe7e | |||
| df219b5134 | |||
| 8cdabe8adf | |||
| 8737067af5 | |||
| 50a99f6356 | |||
| 993c5ce8af | |||
| 47dd338340 | |||
| 87b6c12625 | |||
| b351f6ff22 | |||
| 12817a682d | |||
| 88614c08fe | |||
| 4f5c8e745b | |||
| f30413a744 | |||
| 3b8ce12316 | |||
| 880386e563 | |||
| 266c6c3878 | |||
| 7b033aa7c6 | |||
| efd8372b20 | |||
| 74a30be10b | |||
| 1c521e4831 | |||
| eda43b2b93 | |||
| 593241d2f0 | |||
| 69627bdc64 | |||
| 3fa373c720 | |||
| 083a56c729 | |||
| 88fcf0c2a9 | |||
| 26618f8d50 | |||
| 9f205d465c | |||
| d6e736aaf0 | |||
| 36b28d9b96 | |||
| 66113d7d76 | |||
| aa2e8b402c | |||
| 311f3be5d8 | |||
| 70dcd229cf | |||
| 26fe4a489a | |||
| 2363cf48e7 | |||
| 848294c09b | |||
| 693d935538 | |||
| 16405b9b2b | |||
| 4719cc6d59 | |||
| 98b92d4db7 | |||
| 1bdded7a44 | |||
| 9bfe90dee1 | |||
| c153349c62 | |||
| 5b6b5536fd | |||
| bac22dfe9f | |||
| bca6545288 | |||
| b94a5db879 | |||
| 4a4dcb85ef | |||
| 7b70cb66bc | |||
| cd6522bcc6 | |||
| 8885233c7e | |||
| 7478784343 | |||
| dca187de37 | |||
| fe660a253b | |||
| ad49e5820a | |||
| 4c40e6ce06 | |||
| 44c9797844 | |||
| 652d2923bb | |||
| 85349ce475 | |||
| 92cc2b89f7 | |||
| 078383ea82 | |||
| d27d6a504d | |||
| ec5144feca | |||
| 05e0e44a77 | |||
| 108e88e211 | |||
| a693f64c41 | |||
| 5c0468d469 | |||
| f2b1fc66f2 | |||
| 22302bf224 | |||
| bb6663ebac | |||
| c6e98d5a96 | |||
| d077350ae4 | |||
| f01c840ebe | |||
| ca1500ae90 | |||
| d7f3ca00c7 | |||
| fd8140e091 | |||
| d94fbe9895 | |||
| 7816f20e6a | |||
| 0d3610416c | |||
| 377ad54016 | |||
| 9e794f358b | |||
| 4e17cbb9ea | |||
| 4c98b87486 | |||
| 5b753be213 | |||
| a605e7f622 | |||
| 513488f6b8 | |||
| 43ea4a172a | |||
| d47b59879a | |||
| ef80bcc834 | |||
| eb8bd3894a | |||
| 7e552333a9 | |||
| 213eafa203 | |||
| 7b18ff8870 | |||
| 5246e2ff25 | |||
| dde9214ae4 | |||
| 29b7a41692 | |||
| 216753678a | |||
| b9e67f6565 | |||
| 3a481b5250 | |||
| 20769b4c2f | |||
| 14ac2cff4c | |||
| fde627d955 | |||
| 6942ecc13a | |||
| 963ff14ed0 | |||
| 96a3ded2ec | |||
| a21196ec54 | |||
| 0b83d9932b | |||
| 6bd92ab926 | |||
| 02eccf7762 | |||
| 89cf276779 | |||
| bc701cd529 | |||
| bfd81fc290 | |||
| 0dd8e883b0 | |||
| c31b58e2c9 | |||
| b163045757 | |||
| 41e9ec1364 | |||
| 64544a5726 | |||
| d7d5a7f8f6 | |||
| a451f75917 | |||
| 1515410012 | |||
| 8f9e0d029c | |||
| 90f24da631 | |||
| df70140b36 | |||
| f90eb0cbe4 | |||
| 55e2ea0c3b | |||
| 1d883931b4 | |||
| b65fad09d8 | |||
| 09a559d3c9 | |||
| 9fc749f3d4 | |||
| f836d1c28a | |||
| 4f05a74aa8 | |||
| c30f522ef2 | |||
| 397e704d64 | |||
| acc9d3e409 | |||
| 0c59fc304c | |||
| abd7f1dce3 | |||
| 1d87da00b7 | |||
| 91515ac6dc | |||
| 7ec771f7ec | |||
| a42a5ac696 | |||
| b31c0359eb | |||
| 934e5a6033 | |||
| 690d635505 | |||
| a444efd0eb | |||
| c41f93a468 | |||
| 900da597e4 | |||
| d320833f40 | |||
| c384b2489f | |||
| ddcac86d1d | |||
| 734e3a6d3c | |||
| f18b1a7043 | |||
| 7d24ad23c2 | |||
| 691bc064bb | |||
| 553b1ba852 | |||
| d5592743cb | |||
| 019e75955d | |||
| 32ad545f84 | |||
| 4eddcef1be | |||
| 68776f1cee | |||
| a0e2a15c60 | |||
| 88c6778771 | |||
| 73f6d3366e | |||
| 48a4d5c8a3 | |||
| 6f2f7fa259 | |||
| 49ddf66c2f | |||
| a169e0335d | |||
| e412a0fc6b | |||
| fb5fedbf24 | |||
| 6b04b1e454 | |||
| 0c340ec5ea | |||
| 34679c75a4 | |||
| 1d3820a064 | |||
| 1c749f578c | |||
| 3a887a6e49 | |||
| beef2da628 | |||
| 9b4d73f13a | |||
| 0226d9aec2 | |||
| 902222675a | |||
| ec43493522 | |||
| baa0518912 | |||
| d665079b84 | |||
| f0d935dee1 | |||
| 314b82caa0 | |||
| 8f79139b78 | |||
| c5296b870a | |||
| 78697d1cea | |||
| 852da5714a | |||
| 4f79303811 | |||
| f294d527e1 | |||
| 54a1cd5069 | |||
| 748d90b443 | |||
| 128b01e049 | |||
| 788c9c6c54 | |||
| a10705fb20 | |||
| b01b8afa8c | |||
| acd4cb51aa | |||
| 5ebcae997e | |||
| 2511a98e8b | |||
| a7692d10c4 | |||
| c892f04c96 | |||
| 3aad5a39ea | |||
| 7f025da5b6 | |||
| 01285bdbbe | |||
| 8182484572 | |||
| 0584dd2f1e | |||
| bd559a2660 | |||
| b4add625b2 | |||
| 890bbff007 | |||
| b853d5b124 | |||
| 693e0e09f7 | |||
| d52356b131 | |||
| b11b995d03 | |||
| 99ba295082 | |||
| 8c2b5957eb | |||
| 4472164447 | |||
| a3cbe3514b | |||
| efa7c862a4 | |||
| 0df7a085de | |||
| 6ae51f287c | |||
| 36076d5279 | |||
| 427c4e3982 | |||
| 1632ce87a5 | |||
| c523c80598 | |||
| 0bd6df507b | |||
| 6e41220dbf |
@@ -9,9 +9,23 @@ 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. -->
|
||||
|
||||
<!--
|
||||
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.
|
||||
|
||||
You can also check what process the window PID belongs to:
|
||||
|
||||
$ readlink /proc/$(niri msg --json pick-window | jq .pid)/exe
|
||||
|
||||
If this points to xwayland-satellite, then it's an X11 window.
|
||||
|
||||
Please report issues with X11 apps to xwayland-satellite instead of niri: https://github.com/Supreeeme/xwayland-satellite/issues
|
||||
-->
|
||||
|
||||
### System Information
|
||||
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||
<!-- Paste the output of `niri -V`, e.g. niri 25.02 (b94a5db) -->
|
||||
* niri version:
|
||||
|
||||
<!-- Write your distribution, e.g. Fedora 40 Silverblue -->
|
||||
|
||||
@@ -2,3 +2,9 @@ contact_links:
|
||||
- name: Feature request
|
||||
url: https://github.com/YaLTeR/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
|
||||
about: Question about niri (start a Discussion)
|
||||
- name: Matrix room
|
||||
url: https://matrix.to/#/#niri:matrix.org
|
||||
about: Chat about niri with other users
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
|
||||
env:
|
||||
RUN_SLOW_TESTS: 1
|
||||
DEPS_APT: curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||
DEPS_DNF: cargo gcc clang libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel libdisplay-info-devel
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -68,6 +70,41 @@ jobs:
|
||||
- name: Test
|
||||
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||
|
||||
# Job that runs randomized tests for a longer period of time.
|
||||
randomized-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: randomized tests
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
PROPTEST_CASES: 200000
|
||||
PROPTEST_MAX_LOCAL_REJECTS: 200000
|
||||
PROPTEST_MAX_GLOBAL_REJECTS: 200000
|
||||
PROPTEST_MAX_SHRINK_ITERS: 200000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }}
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build tests
|
||||
run: cargo test --no-run --all --exclude niri-visual-tests --release
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --exclude niri-visual-tests --release
|
||||
|
||||
visual-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -83,7 +120,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -96,7 +133,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: 'msrv - 1.80.1'
|
||||
name: msrv
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
@@ -107,7 +144,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.80.1
|
||||
|
||||
@@ -130,7 +167,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev libadwaita-1-dev
|
||||
sudo apt-get install -y ${{ env.DEPS_APT }} libadwaita-1-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -168,7 +205,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo dnf update -y
|
||||
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libdisplay-info-devel libadwaita-devel
|
||||
sudo dnf install -y ${{ env.DEPS_DNF }} libadwaita-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build --all
|
||||
@@ -191,9 +228,21 @@ jobs:
|
||||
- run: nix flake check
|
||||
continue-on-error: true
|
||||
|
||||
check-links:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: lycheeverse/lychee-action@v2.0.2 # later versions break fragment checks. don't bump until this is fixed: https://github.com/lycheeverse/lychee/issues/1574
|
||||
with:
|
||||
args: --offline --include-fragments 'wiki/*.md'
|
||||
|
||||
publish-wiki:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: build
|
||||
needs:
|
||||
- build
|
||||
- check-links
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -202,7 +251,7 @@ jobs:
|
||||
with:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@b7e552d7cb0fa7f83e459012ffc6840fd87bcb83
|
||||
|
||||
rustdoc:
|
||||
needs: build
|
||||
|
||||
Generated
+471
-446
File diff suppressed because it is too large
Load Diff
+27
-24
@@ -6,31 +6,33 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "25.1.0"
|
||||
version = "25.2.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"
|
||||
rust-version = "1.80.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
bitflags = "2.7.0"
|
||||
clap = { version = "4.5.26", features = ["derive"] }
|
||||
insta = "1.42.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.135"
|
||||
anyhow = "1.0.97"
|
||||
bitflags = "2.9.0"
|
||||
clap = { version = "4.5.34", features = ["derive"] }
|
||||
insta = "1.42.2"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.18.0", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
# version = "0.4.1"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.smithay-drm-extras]
|
||||
# version = "0.1.0"
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
# path = "../smithay/smithay-drm-extras"
|
||||
|
||||
@@ -54,30 +56,31 @@ async-channel = "2.3.1"
|
||||
async-io = { version = "2.4.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.21.0", features = ["derive"] }
|
||||
bytemuck = { version = "1.22.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.2", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
clap_complete = "4.5.47"
|
||||
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"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.29.2"
|
||||
glam = "0.30.1"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.169"
|
||||
libc = "0.2.171"
|
||||
libdisplay-info = "0.2.2"
|
||||
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.1.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.1.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "4.6.0"
|
||||
pango = { version = "0.20.7", features = ["v1_44"] }
|
||||
log = { version = "0.4.27", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "25.2.0", path = "niri-config" }
|
||||
niri-ipc = { version = "25.2.0", path = "niri-ipc", features = ["clap"] }
|
||||
ordered-float = "5.0.0"
|
||||
pango = { version = "0.20.9", features = ["v1_44"] }
|
||||
pangocairo = "0.20.7"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.16"
|
||||
portable-atomic = { version = "1.10.0", default-features = false, features = ["float"] }
|
||||
portable-atomic = { version = "1.11.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.16"
|
||||
sd-notify = "0.4.3"
|
||||
sd-notify = "0.4.5"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
@@ -85,10 +88,10 @@ tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.4", optional = true }
|
||||
wayland-backend = "0.3.7"
|
||||
wayland-scanner = "0.31.5"
|
||||
wayland-backend = "0.3.8"
|
||||
wayland-scanner = "0.31.6"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "5.2.0", optional = true }
|
||||
zbus = { version = "5.5.0", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
workspace = true
|
||||
@@ -115,7 +118,7 @@ insta.workspace = true
|
||||
proptest = "1.6.0"
|
||||
proptest-derive = { version = "0.5.1", features = ["boxed_union"] }
|
||||
rayon = "1.10.0"
|
||||
wayland-client = "0.31.7"
|
||||
wayland-client = "0.31.8"
|
||||
xshell = "0.2.7"
|
||||
|
||||
[features]
|
||||
@@ -149,7 +152,7 @@ insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "25.01"
|
||||
version = "25.02"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
|
||||
@@ -29,11 +29,12 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
## Features
|
||||
|
||||
- Built from the ground up for scrollable tiling
|
||||
- Dynamic workspaces like in GNOME
|
||||
- [Dynamic workspaces](https://github.com/YaLTeR/niri/wiki/Workspaces) like in GNOME
|
||||
- Built-in screenshot UI
|
||||
- Monitor and window screencasting through xdg-desktop-portal-gnome
|
||||
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||
- [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://github.com/YaLTeR/niri/wiki/Tabs)
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#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)
|
||||
@@ -91,6 +92,13 @@ Here are some other projects which implement a similar workflow:
|
||||
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Media
|
||||
|
||||
[niri: Making a Wayland compositor in Rust](https://youtu.be/Kmz8ODolnDg?list=PLRdS-n5seLRqrmWDQY4KDqtRMfIwU0U3T)
|
||||
|
||||
My talk from the 2024 Moscow RustCon about niri, and how I do randomized property testing and profiling, and measure input latency.
|
||||
The talk is in Russian, but I prepared full English subtitles that you can find in YouTube's subtitle language selector.
|
||||
|
||||
## Contact
|
||||
|
||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||
|
||||
Generated
+6
-6
@@ -17,11 +17,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1733064805,
|
||||
"narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
|
||||
"lastModified": 1742707865,
|
||||
"narHash": "sha256-RVQQZy38O3Zb8yoRJhuFgWo/iDIDj0hEdRTVfhOtzRk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
|
||||
"rev": "dd613136ee91f67e5dba3f3f41ac99ae89c5406b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -45,11 +45,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733106880,
|
||||
"narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
|
||||
"lastModified": 1742697269,
|
||||
"narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
|
||||
"rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -32,13 +32,14 @@
|
||||
libinput,
|
||||
seatd,
|
||||
libxkbcommon,
|
||||
mesa,
|
||||
libgbm,
|
||||
pango,
|
||||
pipewire,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
systemd,
|
||||
wayland,
|
||||
installShellFiles,
|
||||
withDbus ? true,
|
||||
withSystemd ? true,
|
||||
withScreencastSupport ? true,
|
||||
@@ -79,6 +80,7 @@
|
||||
nativeBuildInputs = [
|
||||
rustPlatform.bindgenHook
|
||||
pkg-config
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
@@ -90,7 +92,7 @@
|
||||
libinput
|
||||
seatd
|
||||
libxkbcommon
|
||||
mesa # libgbm
|
||||
libgbm
|
||||
pango
|
||||
wayland
|
||||
]
|
||||
@@ -117,6 +119,11 @@
|
||||
|
||||
postInstall =
|
||||
''
|
||||
installShellCompletion --cmd niri \
|
||||
--bash <($out/bin/niri completions bash) \
|
||||
--fish <($out/bin/niri completions fish) \
|
||||
--zsh <($out/bin/niri completions zsh)
|
||||
|
||||
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
||||
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
||||
''
|
||||
|
||||
@@ -11,8 +11,8 @@ repository.workspace = true
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
niri-ipc = { version = "25.1.0", path = "../niri-ipc" }
|
||||
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
|
||||
niri-ipc = { version = "25.2.0", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
@@ -20,5 +20,4 @@ tracy-client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{BlockOutFrom, RegexEq};
|
||||
use crate::{BlockOutFrom, CornerRadius, RegexEq, ShadowRule};
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
pub struct LayerRule {
|
||||
@@ -11,6 +11,10 @@ pub struct LayerRule {
|
||||
pub opacity: Option<f32>,
|
||||
#[knuffel(child, unwrap(argument))]
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
#[knuffel(child, default)]
|
||||
pub shadow: ShadowRule,
|
||||
#[knuffel(child)]
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
|
||||
|
||||
+1669
-328
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -13,7 +13,7 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "0.8.21", optional = true }
|
||||
schemars = { version = "0.8.22", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
|
||||
+1
-1
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=25.1.0"
|
||||
niri-ipc = "=25.2.0"
|
||||
```
|
||||
|
||||
+232
-6
@@ -24,7 +24,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=25.1.0"
|
||||
//! niri-ipc = "=25.2.0"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -63,6 +63,10 @@ pub enum Request {
|
||||
FocusedOutput,
|
||||
/// Request information about the focused window.
|
||||
FocusedWindow,
|
||||
/// Request picking a window and get its information.
|
||||
PickWindow,
|
||||
/// Request picking a color from the screen.
|
||||
PickColor,
|
||||
/// Perform an action.
|
||||
Action(Action),
|
||||
/// Change output configuration temporarily.
|
||||
@@ -129,10 +133,22 @@ pub enum Response {
|
||||
FocusedOutput(Option<Output>),
|
||||
/// Information about the focused window.
|
||||
FocusedWindow(Option<Window>),
|
||||
/// Information about the picked window.
|
||||
PickedWindow(Option<Window>),
|
||||
/// Information about the picked color.
|
||||
PickedColor(Option<PickedColor>),
|
||||
/// Output configuration change result.
|
||||
OutputConfigChanged(OutputConfigChanged),
|
||||
}
|
||||
|
||||
/// Color picked from the screen.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct PickedColor {
|
||||
/// Color values as red, green, blue, each ranging from 0.0 to 1.0.
|
||||
pub rgb: [f64; 3],
|
||||
}
|
||||
|
||||
/// Actions that niri can perform.
|
||||
// Variants in this enum should match the spelling of the ones in niri-config. Most, but not all,
|
||||
// variants from niri-config should be present here.
|
||||
@@ -165,9 +181,23 @@ pub enum Action {
|
||||
delay_ms: Option<u16>,
|
||||
},
|
||||
/// Open the screenshot UI.
|
||||
Screenshot {},
|
||||
Screenshot {
|
||||
/// Whether to show the mouse pointer by default in the screenshot UI.
|
||||
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
show_pointer: bool,
|
||||
},
|
||||
/// Screenshot the focused screen.
|
||||
ScreenshotScreen {},
|
||||
ScreenshotScreen {
|
||||
/// Write the screenshot to disk in addition to putting it in your clipboard.
|
||||
///
|
||||
/// The screenshot is saved according to the `screenshot-path` config setting.
|
||||
#[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.
|
||||
#[cfg_attr(feature = "clap", arg(short = 'p', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
show_pointer: bool,
|
||||
},
|
||||
/// Screenshot a window.
|
||||
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
|
||||
ScreenshotWindow {
|
||||
@@ -176,7 +206,14 @@ pub enum Action {
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
/// Write the screenshot to disk in addition to putting it in your clipboard.
|
||||
///
|
||||
/// The screenshot is saved according to the `screenshot-path` config setting.
|
||||
#[cfg_attr(feature = "clap", arg(short = 'd', long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
write_to_disk: bool,
|
||||
},
|
||||
/// Enable or disable the keyboard shortcuts inhibitor (if any) for the focused surface.
|
||||
ToggleKeyboardShortcutsInhibit {},
|
||||
/// Close a window.
|
||||
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
|
||||
CloseWindow {
|
||||
@@ -198,12 +235,32 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Toggle windowed (fake) fullscreen on a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Toggle windowed (fake) fullscreen on the focused window")
|
||||
)]
|
||||
ToggleWindowedFullscreen {
|
||||
/// Id of the window to toggle windowed fullscreen of.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Focus a window by id.
|
||||
FocusWindow {
|
||||
/// Id of the window to focus.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: u64,
|
||||
},
|
||||
/// Focus a window in the focused column by index.
|
||||
FocusWindowInColumn {
|
||||
/// Index of the window in the column.
|
||||
///
|
||||
/// The index starts from 1 for the topmost window.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: u8,
|
||||
},
|
||||
/// Focus the previously focused window.
|
||||
FocusWindowPrevious {},
|
||||
/// Focus the column to the left.
|
||||
@@ -218,6 +275,14 @@ pub enum Action {
|
||||
FocusColumnRightOrFirst {},
|
||||
/// Focus the next column to the left, looping if at start.
|
||||
FocusColumnLeftOrLast {},
|
||||
/// Focus a column by index.
|
||||
FocusColumn {
|
||||
/// Index of the column to focus.
|
||||
///
|
||||
/// The index starts from 1 for the first column.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: usize,
|
||||
},
|
||||
/// Focus the window or the monitor above.
|
||||
FocusWindowOrMonitorUp {},
|
||||
/// Focus the window or the monitor below.
|
||||
@@ -242,6 +307,14 @@ pub enum Action {
|
||||
FocusWindowOrWorkspaceDown {},
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceUp {},
|
||||
/// Focus the topmost window.
|
||||
FocusWindowTop {},
|
||||
/// Focus the bottommost window.
|
||||
FocusWindowBottom {},
|
||||
/// Focus the window below or the topmost window.
|
||||
FocusWindowDownOrTop {},
|
||||
/// Focus the window above or the bottommost window.
|
||||
FocusWindowUpOrBottom {},
|
||||
/// Move the focused column to the left.
|
||||
MoveColumnLeft {},
|
||||
/// Move the focused column to the right.
|
||||
@@ -254,6 +327,14 @@ pub enum Action {
|
||||
MoveColumnLeftOrToMonitorLeft {},
|
||||
/// Move the focused column to the right or to the monitor to the right.
|
||||
MoveColumnRightOrToMonitorRight {},
|
||||
/// Move the focused column to a specific index on its workspace.
|
||||
MoveColumnToIndex {
|
||||
/// New index for the column.
|
||||
///
|
||||
/// The index starts from 1 for the first column.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: usize,
|
||||
},
|
||||
/// Move the focused window down in a column.
|
||||
MoveWindowDown {},
|
||||
/// Move the focused window up in a column.
|
||||
@@ -290,10 +371,18 @@ pub enum Action {
|
||||
ConsumeWindowIntoColumn {},
|
||||
/// Expel the focused window from the column.
|
||||
ExpelWindowFromColumn {},
|
||||
/// Swap focused window with one to the right
|
||||
/// Swap focused window with one to the right.
|
||||
SwapWindowRight {},
|
||||
/// Swap focused window with one to the left
|
||||
/// Swap focused window with one to the left.
|
||||
SwapWindowLeft {},
|
||||
/// Toggle the focused column between normal and tabbed display.
|
||||
ToggleColumnTabbedDisplay {},
|
||||
/// Set the display mode of the focused column.
|
||||
SetColumnDisplay {
|
||||
/// Display mode to set.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
display: ColumnDisplay,
|
||||
},
|
||||
/// Center the focused column on the screen.
|
||||
CenterColumn {},
|
||||
/// Center a window on the screen.
|
||||
@@ -339,6 +428,14 @@ pub enum Action {
|
||||
/// Reference (index or name) of the workspace to move the window to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
reference: WorkspaceReferenceArg,
|
||||
|
||||
/// Whether the focus should follow the moved window.
|
||||
///
|
||||
/// If `true` (the default) and the window to move is focused, the focus will follow the
|
||||
/// window to the new workspace. If `false`, the focus will remain on the original
|
||||
/// workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long, action = clap::ArgAction::Set, default_value_t = true))]
|
||||
focus: bool,
|
||||
},
|
||||
/// Move the focused column to the workspace below.
|
||||
MoveColumnToWorkspaceDown {},
|
||||
@@ -354,6 +451,22 @@ pub enum Action {
|
||||
MoveWorkspaceDown {},
|
||||
/// Move the focused workspace up.
|
||||
MoveWorkspaceUp {},
|
||||
/// Move a workspace to a specific index on its monitor.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Move the focused workspace to a specific index on its monitor")
|
||||
)]
|
||||
MoveWorkspaceToIndex {
|
||||
/// New index for the workspace.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
index: usize,
|
||||
|
||||
/// Reference (index or name) of the workspace to move.
|
||||
///
|
||||
/// If `None`, uses the focused workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
reference: Option<WorkspaceReferenceArg>,
|
||||
},
|
||||
/// Set the name of a workspace.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
@@ -394,6 +507,12 @@ pub enum Action {
|
||||
FocusMonitorPrevious {},
|
||||
/// Focus the next monitor.
|
||||
FocusMonitorNext {},
|
||||
/// Focus a monitor by name.
|
||||
FocusMonitor {
|
||||
/// Name of the output to focus.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
output: String,
|
||||
},
|
||||
/// Move the focused window to the monitor to the left.
|
||||
MoveWindowToMonitorLeft {},
|
||||
/// Move the focused window to the monitor to the right.
|
||||
@@ -406,6 +525,22 @@ pub enum Action {
|
||||
MoveWindowToMonitorPrevious {},
|
||||
/// Move the focused window to the next monitor.
|
||||
MoveWindowToMonitorNext {},
|
||||
/// Move a window to a specific monitor.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Move the focused window to a specific monitor")
|
||||
)]
|
||||
MoveWindowToMonitor {
|
||||
/// Id of the window to move.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
|
||||
/// The target output name.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
output: String,
|
||||
},
|
||||
/// Move the focused column to the monitor to the left.
|
||||
MoveColumnToMonitorLeft {},
|
||||
/// Move the focused column to the monitor to the right.
|
||||
@@ -418,6 +553,12 @@ pub enum Action {
|
||||
MoveColumnToMonitorPrevious {},
|
||||
/// Move the focused column to the next monitor.
|
||||
MoveColumnToMonitorNext {},
|
||||
/// Move the focused column to a specific monitor.
|
||||
MoveColumnToMonitor {
|
||||
/// The target output name.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
output: String,
|
||||
},
|
||||
/// Change the width of a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
@@ -488,6 +629,8 @@ pub enum Action {
|
||||
#[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Expand the focused column to space not taken up by other fully visible columns.
|
||||
ExpandColumnToAvailableWidth {},
|
||||
/// Switch between keyboard layouts.
|
||||
SwitchLayout {
|
||||
/// Layout to switch to.
|
||||
@@ -508,6 +651,22 @@ pub enum Action {
|
||||
MoveWorkspaceToMonitorPrevious {},
|
||||
/// Move the focused workspace to the next monitor.
|
||||
MoveWorkspaceToMonitorNext {},
|
||||
/// Move a workspace to a specific monitor.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Move the focused workspace to a specific monitor")
|
||||
)]
|
||||
MoveWorkspaceToMonitor {
|
||||
/// The target output name.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
output: String,
|
||||
|
||||
// Reference (index or name) of the workspace to move.
|
||||
///
|
||||
/// If `None`, uses the focused workspace.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
reference: Option<WorkspaceReferenceArg>,
|
||||
},
|
||||
/// Toggle a debug tint on windows.
|
||||
ToggleDebugTint {},
|
||||
/// Toggle visualization of render element opaque regions.
|
||||
@@ -567,6 +726,46 @@ pub enum Action {
|
||||
)]
|
||||
y: PositionChange,
|
||||
},
|
||||
/// Toggle the opacity of a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Toggle the opacity of the focused window")
|
||||
)]
|
||||
ToggleWindowRuleOpacity {
|
||||
/// Id of the window.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Set the dynamic cast target to a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Set the dynamic cast target to the focused window")
|
||||
)]
|
||||
SetDynamicCastWindow {
|
||||
/// Id of the window to target.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Set the dynamic cast target to a monitor.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Set the dynamic cast target to the focused monitor")
|
||||
)]
|
||||
SetDynamicCastMonitor {
|
||||
/// Name of the output to target.
|
||||
///
|
||||
/// If `None`, uses the focused output.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Clear the dynamic cast target, making it show nothing.
|
||||
ClearDynamicCastTarget {},
|
||||
/// Toggle the Overview.
|
||||
ToggleOverview {},
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
@@ -613,6 +812,18 @@ pub enum LayoutSwitchTarget {
|
||||
Next,
|
||||
/// The previous configured layout.
|
||||
Prev,
|
||||
/// The specific layout by index.
|
||||
Index(u8),
|
||||
}
|
||||
|
||||
/// How windows display in a column.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum ColumnDisplay {
|
||||
/// Windows are tiled vertically across the working area height.
|
||||
Normal,
|
||||
/// Windows are in tabs.
|
||||
Tabbed,
|
||||
}
|
||||
|
||||
/// Output actions that niri can perform.
|
||||
@@ -1119,7 +1330,22 @@ impl FromStr for LayoutSwitchTarget {
|
||||
match s {
|
||||
"next" => Ok(Self::Next),
|
||||
"prev" => Ok(Self::Prev),
|
||||
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
|
||||
other => match other.parse() {
|
||||
Ok(layout) => Ok(Self::Index(layout)),
|
||||
_ => Err(r#"invalid layout action, can be "next", "prev" or a layout index"#),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ColumnDisplay {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"normal" => Ok(Self::Normal),
|
||||
"tabbed" => Ok(Self::Tabbed),
|
||||
_ => Err(r#"invalid column display, can be "normal" or "tabbed""#),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.7.2", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.9.5", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.1.0", path = ".." }
|
||||
niri-config = { version = "25.1.0", path = "../niri-config" }
|
||||
gtk = { version = "0.9.6", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "25.2.0", path = ".." }
|
||||
niri-config = { version = "25.2.0", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -63,6 +63,7 @@ impl TestCase for GradientAngle {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -84,6 +84,7 @@ impl TestCase for GradientArea {
|
||||
Rectangle::default(),
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
);
|
||||
rv.extend(
|
||||
self.border
|
||||
@@ -103,6 +104,7 @@ impl TestCase for GradientArea {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientOklab {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -42,6 +42,7 @@ impl TestCase for GradientOklabAlpha {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchAlpha {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchDecreasing {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchIncreasing {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchLonger {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientOklchShorter {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientSrgb {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -42,6 +42,7 @@ impl TestCase for GradientSrgbAlpha {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -44,6 +44,7 @@ impl TestCase for GradientSrgbLinear {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -42,6 +42,7 @@ impl TestCase for GradientSrgbLinearAlpha {
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
|
||||
@@ -2,10 +2,9 @@ use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri::animation::Clock;
|
||||
use niri::layout::scrolling::ColumnWidth;
|
||||
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options};
|
||||
use niri::render_helpers::RenderTarget;
|
||||
use niri_config::{Color, FloatOrInt, OutputName};
|
||||
use niri_config::{Color, FloatOrInt, OutputName, PresetSize};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
@@ -84,13 +83,13 @@ impl Layout {
|
||||
pub fn open_in_between(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||
rv.layout.activate_window(&0);
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
@@ -103,7 +102,7 @@ impl Layout {
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
}
|
||||
@@ -117,7 +116,7 @@ impl Layout {
|
||||
for delay in [100, 200, 300] {
|
||||
rv.add_step(delay, move |l| {
|
||||
let win = TestWindow::freeform(delay as usize);
|
||||
l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.add_window(win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
}
|
||||
@@ -128,13 +127,13 @@ impl Layout {
|
||||
pub fn open_to_the_left(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.3)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3)));
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.3)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
@@ -144,26 +143,27 @@ impl Layout {
|
||||
pub fn open_to_the_left_big(args: Args) -> Self {
|
||||
let mut rv = Self::new(args);
|
||||
|
||||
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8)));
|
||||
rv.add_window(TestWindow::freeform(0), Some(PresetSize::Proportion(0.3)));
|
||||
rv.add_window(TestWindow::freeform(1), Some(PresetSize::Proportion(0.8)));
|
||||
|
||||
rv.add_step(500, |l| {
|
||||
let win = TestWindow::freeform(2);
|
||||
let right_of = l.windows[0].clone();
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5)));
|
||||
l.add_window_right_of(&right_of, win.clone(), Some(PresetSize::Proportion(0.5)));
|
||||
l.layout.start_open_animation_for_window(win.id());
|
||||
});
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<PresetSize>) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
let min_size = window.min_size();
|
||||
let max_size = window.max_size();
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
@@ -184,7 +184,7 @@ impl Layout {
|
||||
&mut self,
|
||||
right_of: &TestWindow,
|
||||
mut window: TestWindow,
|
||||
width: Option<ColumnWidth>,
|
||||
width: Option<PresetSize>,
|
||||
) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
let min_size = window.min_size();
|
||||
@@ -192,6 +192,7 @@ impl Layout {
|
||||
window.request_size(
|
||||
ws.new_window_size(width, None, false, window.rules(), (min_size, max_size)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
window.communicate();
|
||||
@@ -265,6 +266,7 @@ impl TestCase for Layout {
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output, true)
|
||||
.flat_map(|(_, iter)| iter)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use niri::render_helpers::RenderTarget;
|
||||
use niri_config::{Color, FloatOrInt};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Scale, Size};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
|
||||
use super::{Args, TestCase};
|
||||
use crate::test_window::TestWindow;
|
||||
@@ -112,18 +112,13 @@ impl TestCase for Tile {
|
||||
let tile_size = self.tile.tile_size().to_physical(1.);
|
||||
let location = Point::from((size.w - tile_size.w, size.h - tile_size.h)).downscale(2.);
|
||||
|
||||
self.tile.update(
|
||||
self.tile.update_render_elements(
|
||||
true,
|
||||
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
|
||||
1.,
|
||||
);
|
||||
self.tile
|
||||
.render(
|
||||
renderer,
|
||||
location,
|
||||
Scale::from(1.),
|
||||
true,
|
||||
RenderTarget::Output,
|
||||
)
|
||||
.render(renderer, location, true, RenderTarget::Output)
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ pub struct Window {
|
||||
impl Window {
|
||||
pub fn freeform(args: Args) -> Self {
|
||||
let mut window = TestWindow::freeform(0);
|
||||
window.request_size(args.size, false, None);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.request_size(args.size, false, None);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
@@ -29,7 +29,7 @@ impl Window {
|
||||
pub fn fixed_size_with_csd_shadow(args: Args) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(args.size, false, None);
|
||||
window.request_size(args.size, false, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
@@ -38,7 +38,7 @@ impl Window {
|
||||
impl TestCase for Window {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.window
|
||||
.request_size(Size::from((width, height)), false, None);
|
||||
.request_size(Size::from((width, height)), false, false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,19 +17,25 @@ mod imp {
|
||||
use niri::render_helpers::{resources, shaders};
|
||||
use smithay::backend::egl::ffi::egl;
|
||||
use smithay::backend::egl::EGLContext;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
|
||||
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::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
type DynMakeTestCase = Box<dyn Fn(Args) -> Box<dyn TestCase>>;
|
||||
|
||||
struct RendererData {
|
||||
renderer: GlesRenderer,
|
||||
dummy_texture: GlesTexture,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SmithayView {
|
||||
gl_area: gtk::GLArea,
|
||||
size: Cell<(i32, i32)>,
|
||||
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||
renderer: RefCell<Option<Result<RendererData, ()>>>,
|
||||
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||
pub clock: RefCell<Clock>,
|
||||
@@ -125,6 +131,10 @@ mod imp {
|
||||
let Ok(renderer) = renderer else {
|
||||
return Ok(());
|
||||
};
|
||||
let RendererData {
|
||||
renderer,
|
||||
dummy_texture,
|
||||
} = renderer;
|
||||
|
||||
let size = self.size.get();
|
||||
|
||||
@@ -147,16 +157,45 @@ mod imp {
|
||||
|
||||
let rect: Rectangle<i32, Physical> = Rectangle::from_size(Size::from(size));
|
||||
|
||||
let elements = unsafe {
|
||||
with_framebuffer_save_restore(renderer, |renderer| {
|
||||
case.render(renderer, Size::from(size))
|
||||
// Fetch GtkGLArea's framebuffer binding.
|
||||
let mut framebuffer = 0;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.GetIntegerv(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||
&mut framebuffer,
|
||||
);
|
||||
})
|
||||
}?;
|
||||
.context("error running closure in GL context")?;
|
||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||
|
||||
// This call will already change the framebuffer binding (offscreen elements will bind
|
||||
// intermediate textures during rendering).
|
||||
let elements = case.render(renderer, Size::from(size));
|
||||
|
||||
// HACK: there's currently no way to "just" render into an externally bound framebuffer
|
||||
// (like we have in this case). The render() call requires a valid target. So what
|
||||
// we'll do is use a dummy texture as a target, then swap the framebuffer binding right
|
||||
// before rendering.
|
||||
let mut dummy_target = renderer
|
||||
.bind(dummy_texture)
|
||||
.context("error binding dummy texture")?;
|
||||
|
||||
let mut frame = renderer
|
||||
.render(rect.size, Transform::Normal)
|
||||
.render(&mut dummy_target, rect.size, Transform::Normal)
|
||||
.context("error creating frame")?;
|
||||
|
||||
// Now that render() bound the dummy texture, change the binding underneath it back to
|
||||
// GtkGLArea's framebuffer, to render there instead.
|
||||
frame
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
frame
|
||||
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
|
||||
.context("error clearing")?;
|
||||
@@ -177,7 +216,7 @@ mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
|
||||
unsafe fn create_renderer() -> anyhow::Result<RendererData> {
|
||||
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
||||
.context("error loading EGL symbols in Smithay")?;
|
||||
|
||||
@@ -200,40 +239,17 @@ mod imp {
|
||||
|
||||
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
|
||||
|
||||
let dummy_texture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, Size::from((1, 1)))
|
||||
.context("error creating dummy texture")?;
|
||||
|
||||
resources::init(&mut renderer);
|
||||
shaders::init(&mut renderer);
|
||||
|
||||
Ok(renderer)
|
||||
}
|
||||
|
||||
unsafe fn with_framebuffer_save_restore<T>(
|
||||
renderer: &mut GlesRenderer,
|
||||
f: impl FnOnce(&mut GlesRenderer) -> T,
|
||||
) -> anyhow::Result<T> {
|
||||
let mut framebuffer = 0;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.GetIntegerv(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
|
||||
&mut framebuffer,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
ensure!(framebuffer != 0, "error getting the framebuffer");
|
||||
|
||||
let rv = f(renderer);
|
||||
|
||||
renderer.unbind().context("error unbinding")?;
|
||||
renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.BindFramebuffer(
|
||||
smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
|
||||
framebuffer as u32,
|
||||
);
|
||||
})
|
||||
.context("error running closure in GL context")?;
|
||||
|
||||
Ok(rv)
|
||||
Ok(RendererData {
|
||||
renderer,
|
||||
dummy_texture,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ use niri::layout::{
|
||||
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
||||
LayoutElementRenderSnapshot,
|
||||
};
|
||||
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::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::output::{self, Output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Scale, Serial, Size, Transform};
|
||||
@@ -181,15 +182,12 @@ impl LayoutElement for TestWindow {
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
is_fullscreen: bool,
|
||||
_animate: bool,
|
||||
_transaction: Option<Transaction>,
|
||||
) {
|
||||
self.inner.borrow_mut().requested_size = Some(size);
|
||||
self.inner.borrow_mut().pending_fullscreen = false;
|
||||
}
|
||||
|
||||
fn request_fullscreen(&mut self, _size: Size<i32, Logical>) {
|
||||
self.inner.borrow_mut().pending_fullscreen = true;
|
||||
self.inner.borrow_mut().pending_fullscreen = is_fullscreen;
|
||||
}
|
||||
|
||||
fn min_size(&self) -> Size<i32, Logical> {
|
||||
@@ -214,7 +212,7 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn output_leave(&self, _output: &Output) {}
|
||||
|
||||
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
||||
fn set_offscreen_data(&self, _data: Option<OffscreenData>) {}
|
||||
|
||||
fn set_activated(&mut self, _active: bool) {}
|
||||
|
||||
@@ -224,6 +222,10 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||
|
||||
fn is_ignoring_opacity_window_rule(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn configure_intent(&self) -> ConfigureIntent {
|
||||
ConfigureIntent::CanSend
|
||||
}
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ SourceLicense: GPL-3.0-or-later
|
||||
# Unicode-3.0
|
||||
# Unlicense OR MIT
|
||||
# Zlib OR Apache-2.0 OR MIT
|
||||
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) 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 AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) 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 OR Apache-2.0 OR MIT) License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) 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) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) 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 OR Apache-2.0 OR MIT)
|
||||
License: (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) 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 AND (MIT OR Apache-2.0)) AND (MIT OR Apache-2.0) AND ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) 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 OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
@@ -89,6 +89,7 @@ Recommends: gnome-keyring
|
||||
Recommends: alacritty
|
||||
Recommends: fuzzel
|
||||
Recommends: swaylock
|
||||
Recommends: waybar
|
||||
# Suggested utilities
|
||||
Recommends: swaybg
|
||||
Recommends: mako
|
||||
@@ -128,7 +129,6 @@ install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
|
||||
|
||||
%if %{with check}
|
||||
%check
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
%cargo_test -- --workspace --exclude niri-visual-tests
|
||||
%endif
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ input {
|
||||
tap
|
||||
// dwt
|
||||
// dwtp
|
||||
// drag false
|
||||
// drag-lock
|
||||
natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
@@ -191,6 +193,43 @@ layout {
|
||||
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||
}
|
||||
|
||||
// You can enable drop shadows for windows.
|
||||
shadow {
|
||||
// Uncomment the next line to enable shadows.
|
||||
// on
|
||||
|
||||
// By default, the shadow draws only around its window, and not behind it.
|
||||
// Uncomment this setting to make the shadow draw behind its window.
|
||||
//
|
||||
// Note that niri has no way of knowing about the CSD window corner
|
||||
// radius. It has to assume that windows have square corners, leading to
|
||||
// shadow artifacts inside the CSD rounded corners. This setting fixes
|
||||
// those artifacts.
|
||||
//
|
||||
// However, instead you may want to set prefer-no-csd and/or
|
||||
// geometry-corner-radius. Then, niri will know the corner radius and
|
||||
// draw the shadow correctly, without having to draw it behind the
|
||||
// window. These will also remove client-side shadows if the window
|
||||
// draws any.
|
||||
//
|
||||
// draw-behind-window true
|
||||
|
||||
// You can change how shadows look. The values below are in logical
|
||||
// pixels and match the CSS box-shadow properties.
|
||||
|
||||
// Softness controls the shadow blur radius.
|
||||
softness 30
|
||||
|
||||
// Spread expands the shadow.
|
||||
spread 5
|
||||
|
||||
// Offset moves the shadow relative to the window.
|
||||
offset x=0 y=5
|
||||
|
||||
// You can also change the shadow color and opacity.
|
||||
color "#0007"
|
||||
}
|
||||
|
||||
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||
// Left and right struts will cause the next window to the side to always be visible.
|
||||
@@ -208,7 +247,9 @@ layout {
|
||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||
// which may be more convenient to use.
|
||||
// See the binds section below for more spawn examples.
|
||||
// spawn-at-startup "alacritty" "-e" "fish"
|
||||
|
||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||
spawn-at-startup "waybar"
|
||||
|
||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
||||
// If the client will specifically ask for CSD, the request will be honored.
|
||||
@@ -294,9 +335,9 @@ binds {
|
||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||
|
||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||
Mod+T { spawn "alacritty"; }
|
||||
Mod+D { spawn "fuzzel"; }
|
||||
Super+Alt+L { spawn "swaylock"; }
|
||||
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; }
|
||||
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
|
||||
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
|
||||
|
||||
// You can also use a shell. Do this if you need pipes, multiple commands, etc.
|
||||
// Note: the entire command goes as a single argument in the end.
|
||||
@@ -466,6 +507,11 @@ binds {
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
|
||||
// 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; }
|
||||
|
||||
Mod+C { center-column; }
|
||||
|
||||
// Finer width adjustments.
|
||||
@@ -487,6 +533,11 @@ binds {
|
||||
Mod+V { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
|
||||
// Toggle tabbed column display mode.
|
||||
// Windows in this column will appear as vertical tabs,
|
||||
// rather than stacked on top of each other.
|
||||
Mod+W { toggle-column-tabbed-display; }
|
||||
|
||||
// Actions to switch layouts.
|
||||
// Note: if you uncomment these, make sure you do NOT have
|
||||
// a matching layout switch hotkey configured in xkb options above.
|
||||
@@ -499,6 +550,16 @@ binds {
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
|
||||
// Applications such as remote-desktop clients and software KVM switches may
|
||||
// request that niri stops processing the keyboard shortcuts defined here
|
||||
// so they may, for example, forward the key presses as-is to a remote machine.
|
||||
// It's a good idea to bind an escape hatch to toggle the inhibitor,
|
||||
// so a buggy application can't hold your session hostage.
|
||||
//
|
||||
// The allow-inhibiting=false property can be applied to other binds as well,
|
||||
// which ensures niri always processes them, even when an inhibitor is active.
|
||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete { quit; }
|
||||
|
||||
@@ -12,7 +12,7 @@ if [ -n "$SHELL" ] &&
|
||||
fi
|
||||
|
||||
# Try to detect the service manager that is being used
|
||||
if hash systemctl &> /dev/null; then
|
||||
if hash systemctl >/dev/null 2>&1; then
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
@@ -41,15 +41,15 @@ if hash systemctl &> /dev/null; then
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
elif hash dinitctl &> /dev/null; then
|
||||
elif hash dinitctl >/dev/null 2>&1; then
|
||||
# Check that the user dinit daemon is running
|
||||
if ! pgrep -u $(id -u) dinit &> /dev/null; then
|
||||
if ! pgrep -u "$(id -u)" dinit >/dev/null 2>&1; then
|
||||
echo "dinit user daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri &> /dev/null; then
|
||||
if dinitctl --user is-started niri >/dev/null 2>&1; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+30
-6
@@ -123,8 +123,8 @@ impl Animation {
|
||||
),
|
||||
Kind::Spring(spring) => {
|
||||
let spring = Spring {
|
||||
from: self.from,
|
||||
to: self.to,
|
||||
from,
|
||||
to,
|
||||
initial_velocity: self.initial_velocity,
|
||||
params: spring.params,
|
||||
};
|
||||
@@ -242,12 +242,21 @@ impl Animation {
|
||||
self.clock.now() >= self.start_time + self.clamped_duration
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
if self.is_done() {
|
||||
pub fn value_at(&self, at: Duration) -> f64 {
|
||||
if at <= self.start_time {
|
||||
// Return from when at == start_time so that when the animations are off, the behavior
|
||||
// within a single event loop cycle (i.e. no time had passed since the start of an
|
||||
// animation) matches the behavior when the animations are on.
|
||||
return self.from;
|
||||
} else if self.start_time + self.duration <= at {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = self.clock.now().saturating_sub(self.start_time);
|
||||
if self.clock.should_complete_instantly() {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = at.saturating_sub(self.start_time);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
@@ -280,6 +289,10 @@ impl Animation {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value_at(self.clock.now())
|
||||
}
|
||||
|
||||
/// Returns a value that stops at the target value after first reaching it.
|
||||
///
|
||||
/// Best effort; not always exactly precise.
|
||||
@@ -295,11 +308,22 @@ impl Animation {
|
||||
self.to
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from(&self) -> f64 {
|
||||
self.from
|
||||
}
|
||||
|
||||
pub fn start_time(&self) -> Duration {
|
||||
self.start_time
|
||||
}
|
||||
|
||||
pub fn end_time(&self) -> Duration {
|
||||
self.start_time + self.duration
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
}
|
||||
|
||||
pub fn offset(&mut self, offset: f64) {
|
||||
self.from += offset;
|
||||
self.to += offset;
|
||||
|
||||
@@ -54,6 +54,10 @@ impl Spring {
|
||||
return Duration::MAX;
|
||||
}
|
||||
|
||||
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
||||
|
||||
// As first ansatz for the overdamped solution,
|
||||
@@ -166,3 +170,21 @@ impl Spring {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn overdamped_spring_equal_from_to_nan() {
|
||||
let spring = Spring {
|
||||
from: 0.,
|
||||
to: 0.,
|
||||
initial_velocity: 0.,
|
||||
params: SpringParams::new(1.15, 850., 0.0001),
|
||||
};
|
||||
let _ = spring.duration();
|
||||
let _ = spring.clamped_duration();
|
||||
let _ = spring.value_at(Duration::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -2,12 +2,12 @@ use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{Config, ModKey};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::niri::Niri;
|
||||
use crate::utils::id::IdCounter;
|
||||
|
||||
@@ -94,11 +94,16 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mod_key(&self) -> CompositorMod {
|
||||
pub fn mod_key(&self, config: &Config) -> ModKey {
|
||||
match self {
|
||||
Backend::Tty(_) => CompositorMod::Super,
|
||||
Backend::Winit(_) => CompositorMod::Alt,
|
||||
Backend::Headless(_) => CompositorMod::Super,
|
||||
Backend::Winit(_) => config.input.mod_key_nested.unwrap_or({
|
||||
if let Some(ModKey::Alt) = config.input.mod_key {
|
||||
ModKey::Super
|
||||
} else {
|
||||
ModKey::Alt
|
||||
}
|
||||
}),
|
||||
Backend::Tty(_) | Backend::Headless(_) => config.input.mod_key.unwrap_or(ModKey::Super),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -28,7 +28,7 @@ use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
|
||||
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
|
||||
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, RendererSuper};
|
||||
use smithay::backend::session::libseat::LibSeatSession;
|
||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
||||
@@ -101,15 +101,16 @@ pub type TtyRenderer<'render> = MultiRenderer<
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
>;
|
||||
|
||||
pub type TtyFrame<'render, 'frame> = MultiFrame<
|
||||
pub type TtyFrame<'render, 'frame, 'buffer> = MultiFrame<
|
||||
'render,
|
||||
'render,
|
||||
'frame,
|
||||
'buffer,
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
||||
>;
|
||||
|
||||
pub type TtyRendererError<'render> = <TtyRenderer<'render> as Renderer>::Error;
|
||||
pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Error;
|
||||
|
||||
type GbmDrmCompositor = DrmCompositor<
|
||||
GbmAllocator<DrmDeviceFd>,
|
||||
@@ -478,7 +479,7 @@ impl Tty {
|
||||
|
||||
self.refresh_ipc_outputs(niri);
|
||||
|
||||
niri.idle_notifier_state.notify_activity(&niri.seat);
|
||||
niri.notify_activity();
|
||||
niri.monitors_active = true;
|
||||
self.set_monitors_active(true);
|
||||
niri.queue_redraw_all();
|
||||
@@ -547,7 +548,7 @@ impl Tty {
|
||||
}
|
||||
drop(config);
|
||||
|
||||
niri.layout.update_shaders();
|
||||
niri.update_shaders();
|
||||
|
||||
// Create the dmabuf global.
|
||||
let primary_formats = renderer.dmabuf_formats();
|
||||
|
||||
+11
-7
@@ -156,7 +156,7 @@ impl Winit {
|
||||
}
|
||||
drop(config);
|
||||
|
||||
niri.layout.update_shaders();
|
||||
niri.update_shaders();
|
||||
|
||||
niri.add_output(self.output.clone(), None, false);
|
||||
}
|
||||
@@ -190,12 +190,16 @@ impl Winit {
|
||||
}
|
||||
|
||||
// Hand them over to winit.
|
||||
self.backend.bind().unwrap();
|
||||
let age = self.backend.buffer_age().unwrap();
|
||||
let res = self
|
||||
.damage_tracker
|
||||
.render_output(self.backend.renderer(), age, &elements, [0.; 4])
|
||||
.unwrap();
|
||||
let res = {
|
||||
let (renderer, mut framebuffer) = self.backend.bind().unwrap();
|
||||
// FIXME: currently impossible to call due to a mutable borrow.
|
||||
//
|
||||
// let age = self.backend.buffer_age().unwrap();
|
||||
let age = 0;
|
||||
self.damage_tracker
|
||||
.render_output(renderer, &mut framebuffer, age, &elements, [0.; 4])
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
niri.update_primary_scanout_output(output, &res.states);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use niri_ipc::{Action, OutputAction};
|
||||
|
||||
use crate::utils::version;
|
||||
@@ -54,6 +55,8 @@ pub enum Sub {
|
||||
},
|
||||
/// Cause a panic to check if the backtraces are good.
|
||||
Panic,
|
||||
/// Generate shell completions.
|
||||
Completions { shell: Shell },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -72,6 +75,10 @@ pub enum Msg {
|
||||
FocusedOutput,
|
||||
/// Print information about the focused window.
|
||||
FocusedWindow,
|
||||
/// Pick a window with the mouse and print information about it.
|
||||
PickWindow,
|
||||
/// Pick a color from the screen with the mouse.
|
||||
PickColor,
|
||||
/// Perform an action.
|
||||
Action {
|
||||
#[command(subcommand)]
|
||||
|
||||
+17
-15
@@ -4,12 +4,11 @@ use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
@@ -67,7 +66,7 @@ impl CursorManager {
|
||||
let hotspot = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<Mutex<CursorImageAttributes>>()
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
@@ -76,21 +75,24 @@ impl CursorManager {
|
||||
|
||||
RenderCursor::Surface { hotspot, surface }
|
||||
}
|
||||
CursorImageStatus::Named(icon) => self
|
||||
.get_cursor_with_name(icon, scale)
|
||||
.map(|cursor| RenderCursor::Named {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.unwrap_or_else(|| RenderCursor::Named {
|
||||
icon: Default::default(),
|
||||
scale,
|
||||
cursor: self.get_default_cursor(scale),
|
||||
}),
|
||||
CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor {
|
||||
self.get_cursor_with_name(icon, scale)
|
||||
.map(|cursor| RenderCursor::Named {
|
||||
icon,
|
||||
scale,
|
||||
cursor,
|
||||
})
|
||||
.unwrap_or_else(|| RenderCursor::Named {
|
||||
icon: Default::default(),
|
||||
scale,
|
||||
cursor: self.get_default_cursor(scale),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_current_cursor_animated(&self, scale: i32) -> bool {
|
||||
match &self.current_cursor {
|
||||
CursorImageStatus::Hidden => false,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use niri_ipc::PickedColor;
|
||||
use zbus::fdo::{self, RequestNameFlags};
|
||||
use zbus::interface;
|
||||
use zbus::zvariant::OwnedValue;
|
||||
use zbus::{interface, zvariant};
|
||||
|
||||
use super::Start;
|
||||
|
||||
@@ -12,6 +15,7 @@ pub struct Screenshot {
|
||||
|
||||
pub enum ScreenshotToNiri {
|
||||
TakeScreenshot { include_cursor: bool },
|
||||
PickColor(async_channel::Sender<Option<PickedColor>>),
|
||||
}
|
||||
|
||||
pub enum NiriToScreenshot {
|
||||
@@ -47,6 +51,34 @@ impl Screenshot {
|
||||
|
||||
Ok((true, filename))
|
||||
}
|
||||
|
||||
async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
|
||||
warn!("error sending pick color message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
let color = match rx.recv().await {
|
||||
Ok(Some(color)) => color,
|
||||
Ok(None) => {
|
||||
return Err(fdo::Error::Failed("no color picked".to_owned()));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error receiving message from niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = HashMap::new();
|
||||
let [r, g, b] = color.rgb;
|
||||
result.insert(
|
||||
"color".to_string(),
|
||||
zvariant::OwnedValue::try_from(zvariant::Value::from((r, g, b))).unwrap(),
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
|
||||
+29
-2
@@ -45,12 +45,39 @@ impl DBusServers {
|
||||
let mut dbus = Self::default();
|
||||
|
||||
if is_session_instance {
|
||||
let service_channel = ServiceChannel::new(niri.display_handle.clone());
|
||||
let (to_niri, from_service_channel) = calloop::channel::channel();
|
||||
let service_channel = ServiceChannel::new(to_niri);
|
||||
niri.event_loop
|
||||
.insert_source(from_service_channel, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(new_client) => {
|
||||
state.niri.insert_client(new_client);
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
dbus.conn_service_channel = try_start(service_channel);
|
||||
}
|
||||
|
||||
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
||||
let display_config = DisplayConfig::new(backend.ipc_outputs());
|
||||
let (to_niri, from_display_config) = calloop::channel::channel();
|
||||
let display_config = DisplayConfig::new(to_niri, backend.ipc_outputs());
|
||||
niri.event_loop
|
||||
.insert_source(from_display_config, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(new_conf) => {
|
||||
for (name, conf) in new_conf {
|
||||
state.modify_output_config(&name, move |output| {
|
||||
if let Some(new_output) = conf {
|
||||
*output = new_output;
|
||||
} else {
|
||||
output.off = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
state.reload_output_config();
|
||||
}
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
dbus.conn_display_config = try_start(display_config);
|
||||
|
||||
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smithay::utils::Size;
|
||||
use zbus::fdo::RequestNameFlags;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{self, OwnedValue, Type};
|
||||
@@ -10,8 +12,10 @@ use zbus::{fdo, interface};
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::is_laptop_panel;
|
||||
use crate::utils::scale::supported_scales;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
}
|
||||
|
||||
@@ -44,6 +48,17 @@ pub struct LogicalMonitor {
|
||||
properties: HashMap<String, OwnedValue>,
|
||||
}
|
||||
|
||||
// ApplyMonitorsConfig
|
||||
#[derive(Deserialize, Type)]
|
||||
pub struct LogicalMonitorConfiguration {
|
||||
x: i32,
|
||||
y: i32,
|
||||
scale: f64,
|
||||
transform: u32,
|
||||
_is_primary: bool,
|
||||
monitors: Vec<(String, String, HashMap<String, OwnedValue>)>,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.DisplayConfig")]
|
||||
impl DisplayConfig {
|
||||
async fn get_current_state(
|
||||
@@ -55,75 +70,70 @@ impl DisplayConfig {
|
||||
HashMap<String, OwnedValue>,
|
||||
)> {
|
||||
// Construct the DBus response.
|
||||
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
|
||||
.ipc_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
// Take only enabled outputs.
|
||||
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
|
||||
.map(|output| {
|
||||
// Loosely matches the check in Mutter.
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
let mut monitors = Vec::new();
|
||||
let mut logical_monitors = Vec::new();
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||
);
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
);
|
||||
for output in self.ipc_outputs.lock().unwrap().values() {
|
||||
// Loosely matches the check in Mutter.
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
|
||||
let mut modes: Vec<Mode> = output
|
||||
.modes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let niri_ipc::Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = *m;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
let mut properties = HashMap::new();
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||
);
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
);
|
||||
|
||||
Mode {
|
||||
id: format!("{width}x{height}@{refresh:.3}"),
|
||||
width: i32::from(width),
|
||||
height: i32::from(height),
|
||||
refresh_rate: refresh,
|
||||
preferred_scale: 1.,
|
||||
supported_scales: vec![1., 2., 3.],
|
||||
properties: HashMap::from([(
|
||||
String::from("is-preferred"),
|
||||
OwnedValue::from(is_preferred),
|
||||
)]),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
modes[output.current_mode.unwrap()]
|
||||
let mut modes: Vec<Mode> = output
|
||||
.modes
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let niri_ipc::Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
is_preferred,
|
||||
} = *m;
|
||||
let width = i32::from(width);
|
||||
let height = i32::from(height);
|
||||
let refresh_rate = refresh_rate as f64 / 1000.;
|
||||
|
||||
Mode {
|
||||
id: format!("{width}x{height}@{refresh_rate:.3}"),
|
||||
width,
|
||||
height,
|
||||
refresh_rate,
|
||||
preferred_scale: 1.,
|
||||
supported_scales: supported_scales(Size::from((width, height))).collect(),
|
||||
properties: HashMap::from([(
|
||||
String::from("is-preferred"),
|
||||
OwnedValue::from(is_preferred),
|
||||
)]),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if let Some(mode) = output.current_mode {
|
||||
modes[mode]
|
||||
.properties
|
||||
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||
}
|
||||
|
||||
let connector = c.clone();
|
||||
let model = output.model.clone();
|
||||
let make = output.make.clone();
|
||||
let connector = c.clone();
|
||||
let model = output.model.clone();
|
||||
let make = output.make.clone();
|
||||
|
||||
// Serial is used for session restore, so fall back to the connector name if it's
|
||||
// not available.
|
||||
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
||||
// Serial is used for session restore, so fall back to the connector name if it's
|
||||
// not available.
|
||||
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
||||
|
||||
let monitor = Monitor {
|
||||
names: (connector, make, model, serial),
|
||||
modes,
|
||||
properties,
|
||||
};
|
||||
|
||||
let logical = output.logical.as_ref().unwrap();
|
||||
let names = (connector, make, model, serial);
|
||||
|
||||
if let Some(logical) = output.logical.as_ref() {
|
||||
let transform = match logical.transform {
|
||||
niri_ipc::Transform::Normal => 0,
|
||||
niri_ipc::Transform::_90 => 1,
|
||||
@@ -135,35 +145,151 @@ impl DisplayConfig {
|
||||
niri_ipc::Transform::Flipped270 => 7,
|
||||
};
|
||||
|
||||
let logical_monitor = LogicalMonitor {
|
||||
logical_monitors.push(LogicalMonitor {
|
||||
x: logical.x,
|
||||
y: logical.y,
|
||||
scale: logical.scale,
|
||||
transform,
|
||||
is_primary: false,
|
||||
monitors: vec![monitor.names.clone()],
|
||||
monitors: vec![names.clone()],
|
||||
properties: HashMap::new(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(monitor, logical_monitor)
|
||||
})
|
||||
.collect();
|
||||
monitors.push(Monitor {
|
||||
names,
|
||||
modes,
|
||||
properties,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by connector.
|
||||
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
|
||||
monitors.sort_unstable_by(|a, b| a.names.0.cmp(&b.names.0));
|
||||
logical_monitors.sort_unstable_by(|a, b| a.monitors[0].0.cmp(&b.monitors[0].0));
|
||||
|
||||
let (monitors, logical_monitors) = monitors.into_iter().unzip();
|
||||
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
||||
Ok((0, monitors, logical_monitors, properties))
|
||||
}
|
||||
|
||||
async fn apply_monitors_config(
|
||||
&self,
|
||||
_serial: u32,
|
||||
method: u32,
|
||||
logical_monitor_configs: Vec<LogicalMonitorConfiguration>,
|
||||
_properties: HashMap<String, OwnedValue>,
|
||||
) -> fdo::Result<()> {
|
||||
let current_conf = self.ipc_outputs.lock().unwrap();
|
||||
let mut new_conf = HashMap::new();
|
||||
for requested_config in logical_monitor_configs {
|
||||
if requested_config.monitors.len() > 1 {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"Mirroring is not yet supported".to_owned(),
|
||||
));
|
||||
}
|
||||
for (connector, mode, _props) in requested_config.monitors {
|
||||
if !current_conf.values().any(|o| o.name == connector) {
|
||||
return Err(zbus::fdo::Error::Failed(format!(
|
||||
"Connector '{}' not found",
|
||||
connector
|
||||
)));
|
||||
}
|
||||
new_conf.insert(
|
||||
connector.clone(),
|
||||
Some(niri_config::Output {
|
||||
off: false,
|
||||
name: connector,
|
||||
scale: Some(niri_config::FloatOrInt(requested_config.scale)),
|
||||
transform: match requested_config.transform {
|
||||
0 => niri_ipc::Transform::Normal,
|
||||
1 => niri_ipc::Transform::_90,
|
||||
2 => niri_ipc::Transform::_180,
|
||||
3 => niri_ipc::Transform::_270,
|
||||
4 => niri_ipc::Transform::Flipped,
|
||||
5 => niri_ipc::Transform::Flipped90,
|
||||
6 => niri_ipc::Transform::Flipped180,
|
||||
7 => niri_ipc::Transform::Flipped270,
|
||||
x => {
|
||||
return Err(zbus::fdo::Error::Failed(format!(
|
||||
"Unknown transform {}",
|
||||
x
|
||||
)))
|
||||
}
|
||||
},
|
||||
position: Some(niri_config::Position {
|
||||
x: requested_config.x,
|
||||
y: requested_config.y,
|
||||
}),
|
||||
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
|
||||
zbus::fdo::Error::Failed(format!(
|
||||
"Could not parse mode '{}': {}",
|
||||
mode, e
|
||||
))
|
||||
})?),
|
||||
// FIXME: VRR
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if new_conf.is_empty() {
|
||||
return Err(zbus::fdo::Error::Failed(
|
||||
"At least one output must be enabled".to_owned(),
|
||||
));
|
||||
}
|
||||
for output in current_conf.values() {
|
||||
if !new_conf.contains_key(&output.name) {
|
||||
new_conf.insert(output.name.clone(), None);
|
||||
}
|
||||
}
|
||||
if method == 0 {
|
||||
// 0 means "verify", so don't actually apply here
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(err) = self.to_niri.send(new_conf) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
pub async fn monitors_changed(ctxt: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn power_save_mode(&self) -> i32 {
|
||||
-1
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn set_power_save_mode(&self, _mode: i32) -> zbus::Result<()> {
|
||||
Err(zbus::Error::Unsupported)
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn panel_orientation_managed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn apply_monitors_config_allowed(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[zbus(property)]
|
||||
fn night_light_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayConfig {
|
||||
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
|
||||
Self { ipc_outputs }
|
||||
pub fn new(
|
||||
to_niri: calloop::channel::Sender<HashMap<String, Option<niri_config::Output>>>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
to_niri,
|
||||
ipc_outputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
was_started: Arc<AtomicBool>,
|
||||
@@ -93,6 +95,7 @@ struct StreamParameters {
|
||||
pub enum ScreenCastToNiri {
|
||||
StartCast {
|
||||
session_id: usize,
|
||||
stream_id: usize,
|
||||
target: StreamTargetId,
|
||||
cursor_mode: CursorMode,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
@@ -149,7 +152,7 @@ impl Session {
|
||||
debug!("start");
|
||||
|
||||
for (stream, iface) in &*self.streams.lock().unwrap() {
|
||||
stream.start(self.id, iface.signal_emitter().clone());
|
||||
stream.start(iface.signal_emitter().clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,16 +207,20 @@ impl Session {
|
||||
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||
}
|
||||
|
||||
let path = format!(
|
||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
||||
STREAM_ID.fetch_add(1, Ordering::SeqCst)
|
||||
);
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
|
||||
let target = StreamTarget::Output(output);
|
||||
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
|
||||
let stream = Stream::new(
|
||||
stream_id,
|
||||
self.id,
|
||||
target,
|
||||
cursor_mode,
|
||||
self.to_niri.clone(),
|
||||
);
|
||||
match server.at(&path, stream.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
@@ -237,10 +244,8 @@ impl Session {
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(?properties, "record_window");
|
||||
|
||||
let path = format!(
|
||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
||||
STREAM_ID.fetch_add(1, Ordering::SeqCst)
|
||||
);
|
||||
let stream_id = STREAM_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let path = format!("/org/gnome/Mutter/ScreenCast/Stream/u{stream_id}");
|
||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||
|
||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||
@@ -248,7 +253,13 @@ impl Session {
|
||||
let target = StreamTarget::Window {
|
||||
id: properties.window_id,
|
||||
};
|
||||
let stream = Stream::new(target, cursor_mode, self.to_niri.clone());
|
||||
let stream = Stream::new(
|
||||
stream_id,
|
||||
self.id,
|
||||
target,
|
||||
cursor_mode,
|
||||
self.to_niri.clone(),
|
||||
);
|
||||
match server.at(&path, stream.clone()).await {
|
||||
Ok(true) => {
|
||||
let iface = server.interface(&path).await.unwrap();
|
||||
@@ -350,11 +361,15 @@ impl Drop for Session {
|
||||
|
||||
impl Stream {
|
||||
fn new(
|
||||
id: usize,
|
||||
session_id: usize,
|
||||
target: StreamTarget,
|
||||
cursor_mode: CursorMode,
|
||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
session_id,
|
||||
target,
|
||||
cursor_mode,
|
||||
was_started: Arc::new(AtomicBool::new(false)),
|
||||
@@ -362,13 +377,14 @@ impl Stream {
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self, session_id: usize, ctxt: SignalEmitter<'static>) {
|
||||
fn start(&self, ctxt: SignalEmitter<'static>) {
|
||||
if self.was_started.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = ScreenCastToNiri::StartCast {
|
||||
session_id,
|
||||
session_id: self.session_id,
|
||||
stream_id: self.id,
|
||||
target: self.target.make_id(),
|
||||
cursor_mode: self.cursor_mode,
|
||||
signal_ctx: ctxt,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::reexports::wayland_server::DisplayHandle;
|
||||
use zbus::{fdo, interface, zvariant};
|
||||
|
||||
use super::Start;
|
||||
use crate::niri::ClientState;
|
||||
use crate::niri::NewClient;
|
||||
|
||||
pub struct ServiceChannel {
|
||||
display: DisplayHandle,
|
||||
to_niri: calloop::channel::Sender<NewClient>,
|
||||
}
|
||||
|
||||
#[interface(name = "org.gnome.Mutter.ServiceChannel")]
|
||||
@@ -24,22 +22,24 @@ impl ServiceChannel {
|
||||
}
|
||||
|
||||
let (sock1, sock2) = UnixStream::pair().unwrap();
|
||||
let data = Arc::new(ClientState {
|
||||
compositor_state: Default::default(),
|
||||
// Would be nice to thread config here but for now it's fine.
|
||||
can_view_decoration_globals: false,
|
||||
let client = NewClient {
|
||||
client: sock2,
|
||||
restricted: false,
|
||||
// FIXME: maybe you can get the PID from D-Bus somehow?
|
||||
credentials_unknown: true,
|
||||
});
|
||||
self.display.insert_client(sock2, data).unwrap();
|
||||
};
|
||||
if let Err(err) = self.to_niri.send(client) {
|
||||
warn!("error sending message to niri: {err:?}");
|
||||
return Err(fdo::Error::Failed("internal error".to_owned()));
|
||||
}
|
||||
|
||||
Ok(zvariant::OwnedFd::from(std::os::fd::OwnedFd::from(sock1)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceChannel {
|
||||
pub fn new(display: DisplayHandle) -> Self {
|
||||
Self { display }
|
||||
pub fn new(to_niri: calloop::channel::Sender<NewClient>) -> Self {
|
||||
Self { to_niri }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-33
@@ -1,7 +1,7 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use niri_ipc::PositionChange;
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::reexports::calloop::Interest;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
@@ -21,9 +21,9 @@ use smithay::{delegate_compositor, delegate_shm};
|
||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||
use crate::handlers::XDG_ACTIVATION_TOKEN_TIMEOUT;
|
||||
use crate::layout::{ActivateWindow, AddWindowTarget};
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::utils::send_scale_transform;
|
||||
use crate::niri::{CastTarget, ClientState, LockState, State};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{is_mapped, send_scale_transform};
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||
|
||||
impl CompositorHandler for State {
|
||||
@@ -78,14 +78,7 @@ impl CompositorHandler for State {
|
||||
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()) {
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if is_mapped {
|
||||
if is_mapped(surface) {
|
||||
// The toplevel got mapped.
|
||||
let Unmapped {
|
||||
window,
|
||||
@@ -166,7 +159,9 @@ impl CompositorHandler for State {
|
||||
// None. If the configured output is set, that means it was set explicitly
|
||||
// by a window rule or a fullscreen request.
|
||||
.filter(|(_, parent_output)| {
|
||||
output.is_none() || output.as_ref() == Some(*parent_output)
|
||||
parent_output.is_none()
|
||||
|| output.is_none()
|
||||
|| output.as_ref() == *parent_output
|
||||
})
|
||||
.map(|(mapped, _)| mapped.window.clone());
|
||||
|
||||
@@ -223,18 +218,12 @@ impl CompositorHandler for State {
|
||||
// This is a commit of a previously-mapped root or a non-toplevel root.
|
||||
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
let output = output.cloned();
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
let id = mapped.id();
|
||||
|
||||
// This is a commit of a previously-mapped toplevel.
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
let is_mapped = is_mapped(surface);
|
||||
|
||||
// Must start the close animation before window.on_commit().
|
||||
let transaction = Transaction::new();
|
||||
@@ -256,11 +245,8 @@ impl CompositorHandler for State {
|
||||
let active_window = self.niri.layout.focus().map(|m| &m.window);
|
||||
let was_active = active_window == Some(&window);
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.niri
|
||||
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
|
||||
id: id.get(),
|
||||
});
|
||||
.stop_casts_for_target(CastTarget::Window { id: id.get() });
|
||||
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
@@ -280,7 +266,9 @@ impl CompositorHandler for State {
|
||||
let unmapped = Unmapped::new(window);
|
||||
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,7 +311,9 @@ impl CompositorHandler for State {
|
||||
// Popup placement depends on window size which might have changed.
|
||||
self.update_reactive_popups(&window);
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -334,10 +324,12 @@ impl CompositorHandler for State {
|
||||
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
||||
if let Some((mapped, output)) = root_window_output {
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
let output = output.cloned();
|
||||
window.on_commit();
|
||||
self.niri.layout.update_window(&window, None);
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -412,16 +404,23 @@ impl CompositorHandler for State {
|
||||
}
|
||||
|
||||
// This might be a lock surface.
|
||||
if self.niri.is_locked() {
|
||||
for (output, state) in &self.niri.output_state {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
for (output, state) in &self.niri.output_state {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
if matches!(self.niri.lock_state, LockState::WaitingForSurfaces { .. }) {
|
||||
self.niri.maybe_continue_to_locking();
|
||||
} else {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This message can trigger for lock surfaces that had a commit right after we unlocked
|
||||
// the session, but that's ok, we don't need to handle them.
|
||||
trace!("commit on an unrecognized surface: {surface:?}, root: {root_surface:?}");
|
||||
}
|
||||
|
||||
fn destroyed(&mut self, surface: &WlSurface) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use smithay::backend::renderer::utils::with_renderer_surface_state;
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
@@ -13,7 +12,7 @@ use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::layer::{MappedLayer, ResolvedLayerRules};
|
||||
use crate::niri::State;
|
||||
use crate::utils::send_scale_transform;
|
||||
use crate::utils::{is_mapped, send_scale_transform};
|
||||
|
||||
impl WlrLayerShellHandler for State {
|
||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||
@@ -120,22 +119,16 @@ impl State {
|
||||
.unwrap();
|
||||
|
||||
if initial_configure_sent {
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
if is_mapped {
|
||||
if is_mapped(surface) {
|
||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||
|
||||
// Resolve rules for newly mapped layer surfaces.
|
||||
if was_unmapped {
|
||||
let rules = &self.niri.config.borrow().layer_rules;
|
||||
let config = self.niri.config.borrow();
|
||||
let rules = &config.layer_rules;
|
||||
let rules =
|
||||
ResolvedLayerRules::compute(rules, layer, self.niri.is_at_startup);
|
||||
let mapped = MappedLayer::new(layer.clone(), rules);
|
||||
let mapped = MappedLayer::new(layer.clone(), rules, &config);
|
||||
let prev = self
|
||||
.niri
|
||||
.mapped_layer_surfaces
|
||||
|
||||
+130
-40
@@ -11,7 +11,7 @@ use std::time::Duration;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::TabletToolDescriptor;
|
||||
use smithay::backend::input::{InputEvent, TabletToolDescriptor};
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
||||
@@ -35,6 +35,9 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler;
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
||||
use smithay::wayland::security_context::{
|
||||
@@ -44,10 +47,15 @@ use smithay::wayland::selection::data_device::{
|
||||
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
|
||||
ServerDndGrabHandler,
|
||||
};
|
||||
use smithay::wayland::selection::ext_data_control::{
|
||||
DataControlHandler as ExtDataControlHandler, DataControlState as ExtDataControlState,
|
||||
};
|
||||
use smithay::wayland::selection::primary_selection::{
|
||||
set_primary_focus, PrimarySelectionHandler, PrimarySelectionState,
|
||||
};
|
||||
use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState};
|
||||
use smithay::wayland::selection::wlr_data_control::{
|
||||
DataControlHandler as WlrDataControlHandler, DataControlState as WlrDataControlState,
|
||||
};
|
||||
use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
|
||||
use smithay::wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
||||
@@ -58,8 +66,9 @@ use smithay::wayland::xdg_activation::{
|
||||
};
|
||||
use smithay::{
|
||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||
delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify,
|
||||
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
||||
delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale,
|
||||
delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||
delegate_keyboard_shortcuts_inhibit, delegate_output, delegate_pointer_constraints,
|
||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
||||
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
|
||||
@@ -67,7 +76,8 @@ use smithay::{
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
use crate::niri::{ClientState, DndIcon, State};
|
||||
use crate::layout::ActivateWindow;
|
||||
use crate::niri::{DndIcon, NewClient, State};
|
||||
use crate::protocols::foreign_toplevel::{
|
||||
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||
};
|
||||
@@ -75,10 +85,15 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt
|
||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||
use crate::protocols::virtual_pointer::{
|
||||
VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler,
|
||||
VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent,
|
||||
VirtualPointerMotionEvent,
|
||||
};
|
||||
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
||||
use crate::{
|
||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
||||
delegate_output_management, delegate_screencopy,
|
||||
delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
|
||||
};
|
||||
|
||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -243,7 +258,28 @@ impl InputMethodHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardShortcutsInhibitHandler for State {
|
||||
fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState {
|
||||
&mut self.niri.keyboard_shortcuts_inhibit_state
|
||||
}
|
||||
|
||||
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
// FIXME: show a confirmation dialog with a "remember for this application" kind of toggle.
|
||||
inhibitor.activate();
|
||||
self.niri
|
||||
.keyboard_shortcuts_inhibiting_surfaces
|
||||
.insert(inhibitor.wl_surface().clone(), inhibitor);
|
||||
}
|
||||
|
||||
fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
self.niri
|
||||
.keyboard_shortcuts_inhibiting_surfaces
|
||||
.remove(&inhibitor.wl_surface().clone());
|
||||
}
|
||||
}
|
||||
|
||||
delegate_input_method_manager!(State);
|
||||
delegate_keyboard_shortcuts_inhibit!(State);
|
||||
delegate_virtual_keyboard_manager!(State);
|
||||
|
||||
impl SelectionHandler for State {
|
||||
@@ -309,17 +345,19 @@ impl ClientDndGrabHandler for State {
|
||||
fn dropped(&mut self, target: Option<WlSurface>, validated: bool, _seat: Seat<Self>) {
|
||||
trace!("client dropped, target: {target:?}, validated: {validated}");
|
||||
|
||||
// End DnD before activating a specific window below so that it takes precedence.
|
||||
self.niri.layout.dnd_end();
|
||||
|
||||
// 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() {
|
||||
if let Some(root) = self.niri.root_surface.get(&target) {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
activate_output = false;
|
||||
}
|
||||
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);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
activate_output = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +373,7 @@ impl ClientDndGrabHandler for State {
|
||||
// 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.activate_output(output);
|
||||
self.niri.layout.focus_output(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,14 +395,22 @@ impl PrimarySelectionHandler for State {
|
||||
}
|
||||
delegate_primary_selection!(State);
|
||||
|
||||
impl DataControlHandler for State {
|
||||
fn data_control_state(&self) -> &DataControlState {
|
||||
&self.niri.data_control_state
|
||||
impl WlrDataControlHandler for State {
|
||||
fn data_control_state(&self) -> &WlrDataControlState {
|
||||
&self.niri.wlr_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_data_control!(State);
|
||||
|
||||
impl ExtDataControlHandler for State {
|
||||
fn data_control_state(&self) -> &ExtDataControlState {
|
||||
&self.niri.ext_data_control_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_ext_data_control!(State);
|
||||
|
||||
impl OutputHandler for State {
|
||||
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
|
||||
foreign_toplevel::on_output_bound(self, &output, &wl_output);
|
||||
@@ -406,9 +452,7 @@ impl SessionLockHandler for State {
|
||||
fn unlock(&mut self) {
|
||||
self.niri.unlock();
|
||||
self.niri.activate_monitors(&mut self.backend);
|
||||
self.niri
|
||||
.idle_notifier_state
|
||||
.notify_activity(&self.niri.seat);
|
||||
self.niri.notify_activity();
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
@@ -442,19 +486,12 @@ impl SecurityContextHandler for State {
|
||||
self.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |client, _, state| {
|
||||
let config = state.niri.config.borrow();
|
||||
let data = Arc::new(ClientState {
|
||||
compositor_state: Default::default(),
|
||||
can_view_decoration_globals: config.prefer_no_csd,
|
||||
trace!("inserting a new restricted client, context={context:?}");
|
||||
state.niri.insert_client(NewClient {
|
||||
client,
|
||||
restricted: true,
|
||||
credentials_unknown: false,
|
||||
});
|
||||
|
||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||
warn!("error inserting client: {err}");
|
||||
} else {
|
||||
trace!("inserted a new restricted client, context={context:?}");
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -514,10 +551,13 @@ impl ForeignToplevelHandler for State {
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
&requested_output,
|
||||
None,
|
||||
ActivateWindow::Smart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +602,31 @@ impl ScreencopyHandler for State {
|
||||
}
|
||||
delegate_screencopy!(State);
|
||||
|
||||
impl VirtualPointerHandler for State {
|
||||
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState {
|
||||
&mut self.niri.virtual_pointer_state
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event });
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) {
|
||||
self.process_input_event(
|
||||
InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event },
|
||||
);
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event });
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) {
|
||||
self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event });
|
||||
}
|
||||
}
|
||||
delegate_virtual_pointer!(State);
|
||||
|
||||
impl DrmLeaseHandler for State {
|
||||
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||
self.backend
|
||||
@@ -648,7 +713,12 @@ impl XdgActivationHandler for State {
|
||||
}
|
||||
|
||||
fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool {
|
||||
// Only tokens that were created while the application has keyboard focus are valid.
|
||||
// Tokens without a serial are urgency-only. This is not specified, but it seems to be the
|
||||
// common client behavior.
|
||||
//
|
||||
// We don't have urgency yet, so just ignore such tokens.
|
||||
//
|
||||
// See also: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/150
|
||||
let Some((serial, seat)) = data.serial else {
|
||||
return false;
|
||||
};
|
||||
@@ -656,11 +726,31 @@ impl XdgActivationHandler for State {
|
||||
return false;
|
||||
};
|
||||
|
||||
let keyboard = seat.get_keyboard().unwrap();
|
||||
keyboard
|
||||
.last_enter()
|
||||
.map(|last_enter| serial.is_no_older_than(&last_enter))
|
||||
.unwrap_or(false)
|
||||
// Widely-used clients such as Discord and Telegram make new tokens (with invalid serials)
|
||||
// upon clicking on their tray icon or on their notification. This debug flag makes that
|
||||
// work.
|
||||
//
|
||||
// Clicking on a notification sends clients a perfectly valid activation token from the
|
||||
// notification daemon, but alas they ignore it. Maybe in the future the clients are fixed,
|
||||
// and we can remove this debug flag.
|
||||
let config = self.niri.config.borrow();
|
||||
if config.debug.honor_xdg_activation_with_invalid_serial {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the serial against both a keyboard and a pointer, since layer-shell surfaces
|
||||
// with no keyboard interactivity won't have any keyboard focus.
|
||||
let kb_last_enter = seat.get_keyboard().unwrap().last_enter();
|
||||
if kb_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let pointer_last_enter = seat.get_pointer().unwrap().last_enter();
|
||||
if pointer_last_enter.is_some_and(|last_enter| serial.is_no_older_than(&last_enter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn request_activation(
|
||||
|
||||
+73
-46
@@ -1,6 +1,7 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use calloop::Interest;
|
||||
use niri_config::PresetSize;
|
||||
use smithay::desktop::{
|
||||
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
|
||||
@@ -42,10 +43,12 @@ use crate::input::resize_grab::ResizeGrab;
|
||||
use crate::input::touch_move_grab::TouchMoveGrab;
|
||||
use crate::input::touch_resize_grab::TouchResizeGrab;
|
||||
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
|
||||
use crate::layout::scrolling::ColumnWidth;
|
||||
use crate::niri::{PopupGrabState, State};
|
||||
use crate::layout::ActivateWindow;
|
||||
use crate::niri::{CastTarget, PopupGrabState, State};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
|
||||
use crate::utils::{
|
||||
get_monotonic_time, output_matches_name, send_scale_transform, update_tiled_state, ResizeEdge,
|
||||
};
|
||||
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
|
||||
|
||||
impl XdgShellHandler for State {
|
||||
@@ -123,6 +126,10 @@ impl XdgShellHandler for State {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(output) = output else {
|
||||
return;
|
||||
};
|
||||
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
|
||||
@@ -146,7 +153,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = MoveGrab::new(start_data, window);
|
||||
let grab = MoveGrab::new(start_data, window, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
@@ -309,6 +316,9 @@ impl XdgShellHandler for State {
|
||||
} else if let Some(output) = self.niri.layout.active_output() {
|
||||
let layers = layer_map_for_output(output);
|
||||
|
||||
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
|
||||
// in update_keyboard_focus().
|
||||
|
||||
if layers
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.is_none()
|
||||
@@ -429,23 +439,26 @@ impl XdgShellHandler for State {
|
||||
if let Some((mapped, current_output)) = self
|
||||
.niri
|
||||
.layout
|
||||
.find_window_and_output(toplevel.wl_surface())
|
||||
.find_window_and_output_mut(toplevel.wl_surface())
|
||||
{
|
||||
// A configure is required in response to this event regardless if there are pending
|
||||
// changes.
|
||||
mapped.set_needs_configure();
|
||||
|
||||
let window = mapped.window.clone();
|
||||
|
||||
if let Some(requested_output) = requested_output {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
if Some(&requested_output) != current_output {
|
||||
self.niri.layout.move_to_output(
|
||||
Some(&window),
|
||||
&requested_output,
|
||||
None,
|
||||
ActivateWindow::Smart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.set_fullscreen(&window, true);
|
||||
|
||||
// A configure is required in response to this event regardless if there are pending
|
||||
// changes.
|
||||
toplevel.send_configure();
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||
match &mut unmapped.state {
|
||||
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
||||
@@ -467,7 +480,7 @@ impl XdgShellHandler for State {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
})
|
||||
@@ -509,17 +522,14 @@ impl XdgShellHandler for State {
|
||||
if let Some((mapped, _)) = self
|
||||
.niri
|
||||
.layout
|
||||
.find_window_and_output(toplevel.wl_surface())
|
||||
.find_window_and_output_mut(toplevel.wl_surface())
|
||||
{
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
|
||||
// A configure is required in response to this event regardless if there are pending
|
||||
// changes.
|
||||
//
|
||||
// FIXME: when unfullscreening to floating, this will send an extra configure with
|
||||
// scrolling layout bounds. We should probably avoid it.
|
||||
toplevel.send_configure();
|
||||
mapped.set_needs_configure();
|
||||
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.set_fullscreen(&window, false);
|
||||
} else if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||
match &mut unmapped.state {
|
||||
InitialConfigureState::NotConfigured { wants_fullscreen } => {
|
||||
@@ -556,7 +566,7 @@ impl XdgShellHandler for State {
|
||||
.and_then(|parent| {
|
||||
self.niri.layout.find_window_and_output(&parent)
|
||||
})
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
})
|
||||
@@ -591,7 +601,7 @@ impl XdgShellHandler for State {
|
||||
let configure_width = if is_floating {
|
||||
*floating_width
|
||||
} else if *is_full_width {
|
||||
Some(ColumnWidth::Proportion(1.))
|
||||
Some(PresetSize::Proportion(1.))
|
||||
} else {
|
||||
*width
|
||||
};
|
||||
@@ -642,13 +652,11 @@ impl XdgShellHandler for State {
|
||||
return;
|
||||
};
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
let output = output.cloned();
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.niri
|
||||
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
|
||||
id: mapped.id().get(),
|
||||
});
|
||||
self.niri.stop_casts_for_target(CastTarget::Window {
|
||||
id: mapped.id().get(),
|
||||
});
|
||||
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
@@ -678,7 +686,9 @@ impl XdgShellHandler for State {
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
if let Some(output) = output {
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
}
|
||||
|
||||
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
||||
@@ -738,7 +748,13 @@ impl XdgDecorationHandler for State {
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
toplevel.send_configure();
|
||||
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
|
||||
let surface = toplevel.wl_surface();
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
|
||||
mapped.set_needs_configure();
|
||||
} else {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,14 +767,20 @@ impl XdgDecorationHandler for State {
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
toplevel.send_configure();
|
||||
// If this is a mapped window, flag it as needs configure to avoid duplicate configures.
|
||||
let surface = toplevel.wl_surface();
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) {
|
||||
mapped.set_needs_configure();
|
||||
} else {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_xdg_decoration!(State);
|
||||
|
||||
/// Whether KDE server decorations are in use.
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct KdeDecorationsModeState {
|
||||
server: Cell<bool>,
|
||||
}
|
||||
@@ -862,7 +884,7 @@ impl State {
|
||||
toplevel
|
||||
.parent()
|
||||
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||
.map(|(_win, output)| output)
|
||||
.and_then(|(_win, output)| output)
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
.map(|mon| (mon, true))
|
||||
});
|
||||
@@ -917,7 +939,7 @@ impl State {
|
||||
let configure_width = if is_floating {
|
||||
floating_width
|
||||
} else if is_full_width {
|
||||
Some(ColumnWidth::Proportion(1.))
|
||||
Some(PresetSize::Proportion(1.))
|
||||
} else {
|
||||
width
|
||||
};
|
||||
@@ -931,16 +953,8 @@ impl State {
|
||||
);
|
||||
}
|
||||
|
||||
// If the user prefers no CSD, it's a reasonable assumption that they would prefer to get
|
||||
// rid of the various client-side rounded corners also by using the tiled state.
|
||||
if config.prefer_no_csd {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::TiledLeft);
|
||||
state.states.set(xdg_toplevel::State::TiledRight);
|
||||
state.states.set(xdg_toplevel::State::TiledTop);
|
||||
state.states.set(xdg_toplevel::State::TiledBottom);
|
||||
});
|
||||
}
|
||||
// Set the tiled state for the initial configure.
|
||||
update_tiled_state(toplevel, config.prefer_no_csd, rules.tiled_state);
|
||||
|
||||
// Set the configured settings.
|
||||
*state = InitialConfigureState::Configured {
|
||||
@@ -1051,6 +1065,19 @@ impl State {
|
||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||
// we will compute that here.
|
||||
let mut target = Rectangle::from_size(output_geo.size);
|
||||
|
||||
// Background and bottom layer popups render below the top and the overlay layer, so let's
|
||||
// put them into the non-exclusive zone.
|
||||
//
|
||||
// FIXME: ideally this should use the "top and overlay layer" non-exclusive zone, but
|
||||
// Smithay only computes the "all layers" non-exclusive zone atm.
|
||||
//
|
||||
// FIXME: related to the above, top layer popups should use the "overlay layer"
|
||||
// non-exclusive zone.
|
||||
if matches!(layer_surface.layer(), Layer::Background | Layer::Bottom) {
|
||||
target = map.non_exclusive_zone();
|
||||
}
|
||||
|
||||
target.loc -= layer_geo.loc;
|
||||
target.loc -= get_popup_toplevel_coords(popup);
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use ::input as libinput;
|
||||
use smithay::backend::input;
|
||||
use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::protocols::virtual_pointer::VirtualPointer;
|
||||
|
||||
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
|
||||
type NiriDevice: NiriInputDevice;
|
||||
}
|
||||
impl<T: input::InputBackend> NiriInputBackend for T
|
||||
where
|
||||
Self::Device: NiriInputDevice,
|
||||
{
|
||||
type NiriDevice = Self::Device;
|
||||
}
|
||||
|
||||
pub trait NiriInputDevice: input::Device {
|
||||
// FIXME: this should maybe be per-event, not per-device,
|
||||
// but it's not clear that this matters in practice?
|
||||
// it might be more obvious once we implement it for libinput
|
||||
fn output(&self, state: &State) -> Option<Output>;
|
||||
}
|
||||
|
||||
impl NiriInputDevice for libinput::Device {
|
||||
fn output(&self, _state: &State) -> Option<Output> {
|
||||
// FIXME: Allow specifying the output per-device?
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for WinitVirtualDevice {
|
||||
fn output(&self, _state: &State) -> Option<Output> {
|
||||
// FIXME: we should be returning the single output that the winit backend creates,
|
||||
// but for now, that will cause issues because the output is normally upside down,
|
||||
// so we apply Transform::Flipped180 to it and that would also cause
|
||||
// the cursor position to be flipped, which is not what we want.
|
||||
//
|
||||
// instead, we just return None and rely on the fact that it has only one output.
|
||||
// doing so causes the cursor to be placed in *global* output coordinates,
|
||||
// which are not flipped, and happen to be what we want.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl NiriInputDevice for VirtualPointer {
|
||||
fn output(&self, _: &State) -> Option<Output> {
|
||||
self.output().cloned()
|
||||
}
|
||||
}
|
||||
+954
-192
File diff suppressed because it is too large
Load Diff
+42
-36
@@ -1,12 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
|
||||
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
|
||||
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||
RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
@@ -17,16 +16,32 @@ pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
is_moving: bool,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
Move,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
use_threshold: bool,
|
||||
) -> Self {
|
||||
let gesture = if use_threshold {
|
||||
GestureState::Recognizing
|
||||
} else {
|
||||
GestureState::Move
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
is_moving: false,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
self.gesture = GestureState::Move;
|
||||
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
|
||||
if self.gesture != GestureState::Move {
|
||||
return;
|
||||
}
|
||||
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
@@ -64,14 +97,6 @@ impl PointerGrab<State> for MoveGrab {
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if self.is_moving {
|
||||
data.niri.layout.view_offset_gesture_update(
|
||||
-event_delta.x,
|
||||
timestamp,
|
||||
false,
|
||||
);
|
||||
}
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
@@ -104,25 +129,6 @@ impl PointerGrab<State> for MoveGrab {
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// MouseButton::Middle
|
||||
if event.button == 0x112 {
|
||||
if event.state == ButtonState::Pressed {
|
||||
let output = data
|
||||
.niri
|
||||
.output_under(handle.current_location())
|
||||
.map(|(output, _)| output)
|
||||
.cloned();
|
||||
// FIXME: workspace switch gesture.
|
||||
if let Some(output) = output {
|
||||
self.is_moving = true;
|
||||
data.niri.layout.view_offset_gesture_begin(&output, false);
|
||||
}
|
||||
} else if event.state == ButtonState::Released {
|
||||
self.is_moving = false;
|
||||
data.niri.layout.view_offset_gesture_end(false, None);
|
||||
}
|
||||
}
|
||||
|
||||
// When moving with the left button, right toggles floating, and vice versa.
|
||||
let toggle_floating_button = if self.start_data.button == 0x110 {
|
||||
0x111
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
use niri_ipc::PickedColor;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
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};
|
||||
|
||||
pub struct PickColorGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
}
|
||||
|
||||
impl PickColorGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||
Self { start_data }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
if let Some(tx) = state.niri.pick_color.take() {
|
||||
let _ = tx.send_blocking(None);
|
||||
}
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
|
||||
fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
|
||||
let (output, pos_within_output) = data.niri.output_under(location)?;
|
||||
let output = output.clone();
|
||||
|
||||
data.backend
|
||||
.with_primary_renderer(|renderer| {
|
||||
data.niri.update_render_elements(Some(&output));
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
// FIXME: perhaps replace floor with round once we figure out the pointer behavior
|
||||
// at the bottom/right edges of the monitors.
|
||||
let pos = pos_within_output.to_physical_precise_floor(scale);
|
||||
let size = Size::<i32, Physical>::from((1, 1));
|
||||
|
||||
let elements = data.niri.render(
|
||||
renderer,
|
||||
&output,
|
||||
false,
|
||||
// This is an interactive operation so we can render without blocking out.
|
||||
RenderTarget::Output,
|
||||
);
|
||||
|
||||
let pixels = match render_to_vec(
|
||||
renderer,
|
||||
size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements.iter().rev().map(|elem| {
|
||||
let offset = pos.upscale(-1);
|
||||
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||
}),
|
||||
) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if pixels.len() == 4 {
|
||||
let rgb = [
|
||||
f64::from(pixels[0]) / 255.0,
|
||||
f64::from(pixels[1]) / 255.0,
|
||||
f64::from(pixels[2]) / 255.0,
|
||||
];
|
||||
Some(PickedColor { rgb })
|
||||
} else {
|
||||
error!(
|
||||
"unexpected pixel data length: {} (expected 4)",
|
||||
pixels.len()
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for PickColorGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
if event.state != ButtonState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're handling this press, don't send the release to the window.
|
||||
data.niri.suppressed_buttons.insert(event.button);
|
||||
|
||||
if let Some(tx) = data.niri.pick_color.take() {
|
||||
let color = Self::pick_color_at_point(handle.current_location(), data);
|
||||
let _ = tx.send_blocking(color);
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::window::Mapped;
|
||||
|
||||
pub struct PickWindowGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
}
|
||||
|
||||
impl PickWindowGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>) -> Self {
|
||||
Self { start_data }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
if let Some(tx) = state.niri.pick_window.take() {
|
||||
let _ = tx.send_blocking(None);
|
||||
}
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
// Redraw to update the cursor.
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for PickWindowGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
if event.state != ButtonState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're handling this press, don't send the release to the window.
|
||||
data.niri.suppressed_buttons.insert(event.button);
|
||||
|
||||
if let Some(tx) = data.niri.pick_window.take() {
|
||||
let _ = tx.send_blocking(
|
||||
data.niri
|
||||
.window_under(handle.current_location())
|
||||
.map(Mapped::id),
|
||||
);
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,14 @@ use smithay::input::SeatHandler;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point};
|
||||
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
@@ -27,12 +29,24 @@ enum GestureState {
|
||||
}
|
||||
|
||||
impl SpatialMovementGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, output: Output) -> Self {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
GestureState::Recognizing
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
output,
|
||||
gesture: GestureState::Recognizing,
|
||||
workspace_id,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +95,16 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
if c.x.abs() > c.y.abs() {
|
||||
self.gesture = GestureState::ViewOffset;
|
||||
layout.view_offset_gesture_begin(&self.output, false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
if let Some((ws_idx, ws)) = layout.find_workspace_by_id(self.workspace_id) {
|
||||
if ws.current_output() == Some(&self.output) {
|
||||
layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
|
||||
layout.view_offset_gesture_update(-c.x, timestamp, false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.gesture = GestureState::WorkspaceSwitch;
|
||||
layout.workspace_switch_gesture_begin(&self.output, false);
|
||||
@@ -105,7 +127,7 @@ impl PointerGrab<State> for SpatialMovementGrab {
|
||||
data.niri.queue_redraw(&output);
|
||||
}
|
||||
} else {
|
||||
// The resize is no longer ongoing.
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Msg::Outputs => Request::Outputs,
|
||||
Msg::FocusedWindow => Request::FocusedWindow,
|
||||
Msg::FocusedOutput => Request::FocusedOutput,
|
||||
Msg::PickWindow => Request::PickWindow,
|
||||
Msg::PickColor => Request::PickColor,
|
||||
Msg::Action { action } => Request::Action(action.clone()),
|
||||
Msg::Output { output, action } => Request::Output {
|
||||
output: output.clone(),
|
||||
@@ -252,6 +254,43 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("No output is focused.");
|
||||
}
|
||||
}
|
||||
Msg::PickWindow => {
|
||||
let Response::PickedWindow(window) = response else {
|
||||
bail!("unexpected response: expected PickedWindow, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let window = serde_json::to_string(&window).context("error formatting response")?;
|
||||
println!("{window}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(window) = window {
|
||||
print_window(&window);
|
||||
} else {
|
||||
println!("No window selected.");
|
||||
}
|
||||
}
|
||||
Msg::PickColor => {
|
||||
let Response::PickedColor(color) = response else {
|
||||
bail!("unexpected response: expected PickedColor, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let color = serde_json::to_string(&color).context("error formatting response")?;
|
||||
println!("{color}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(color) = color {
|
||||
let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
|
||||
|
||||
println!("Picked color: rgb({r}, {g}, {b})",);
|
||||
println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
|
||||
} else {
|
||||
println!("No color was picked.");
|
||||
}
|
||||
}
|
||||
Msg::Action { .. } => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
|
||||
+77
-21
@@ -1,5 +1,6 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
@@ -17,12 +18,17 @@ use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, Focus, GrabStartData as PointerGrabStartData,
|
||||
};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::rustix::fs::unlink;
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
use smithay::wayland::shell::wlr_layer::{KeyboardInteractivity, Layer};
|
||||
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::input::pick_window_grab::PickWindowGrab;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::{version, with_toplevel_role};
|
||||
@@ -33,7 +39,10 @@ use crate::window::Mapped;
|
||||
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
|
||||
|
||||
pub struct IpcServer {
|
||||
pub socket_path: PathBuf,
|
||||
/// Path to the IPC socket.
|
||||
///
|
||||
/// This is `None` when creating `IpcServer` without a socket.
|
||||
pub socket_path: Option<PathBuf>,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
@@ -60,31 +69,38 @@ struct EventStreamSender {
|
||||
impl IpcServer {
|
||||
pub fn start(
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
wayland_socket_name: &str,
|
||||
wayland_socket_name: Option<&OsStr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("Ipc::start");
|
||||
|
||||
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||
let mut socket_path = socket_dir();
|
||||
socket_path.push(socket_name);
|
||||
let socket_path = if let Some(wayland_socket_name) = wayland_socket_name {
|
||||
let wayland_socket_name = wayland_socket_name.to_string_lossy();
|
||||
let socket_name = format!("niri.{wayland_socket_name}.{}.sock", process::id());
|
||||
let mut socket_path = socket_dir();
|
||||
socket_path.push(socket_name);
|
||||
|
||||
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.context("error setting socket to non-blocking")?;
|
||||
let listener = UnixListener::bind(&socket_path).context("error binding socket")?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.context("error setting socket to non-blocking")?;
|
||||
|
||||
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(source, |_, socket, state| {
|
||||
match socket.accept() {
|
||||
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let source = Generic::new(listener, Interest::READ, Mode::Level);
|
||||
event_loop
|
||||
.insert_source(source, |_, socket, state| {
|
||||
match socket.accept() {
|
||||
Ok((stream, _)) => on_new_ipc_client(state, stream),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
Ok(PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Some(socket_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
socket_path,
|
||||
@@ -119,7 +135,9 @@ impl IpcServer {
|
||||
|
||||
impl Drop for IpcServer {
|
||||
fn drop(&mut self) {
|
||||
let _ = unlink(&self.socket_path);
|
||||
if let Some(socket_path) = &self.socket_path {
|
||||
let _ = unlink(socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +327,44 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let window = windows.values().find(|win| win.is_focused).cloned();
|
||||
Response::FocusedWindow(window)
|
||||
}
|
||||
Request::PickWindow => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let pointer = state.niri.seat.get_pointer().unwrap();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: 0,
|
||||
location: pointer.current_location(),
|
||||
};
|
||||
let grab = PickWindowGrab::new(start_data);
|
||||
// The `WindowPickGrab` ungrab handler will cancel the previous ongoing pick, if
|
||||
// any.
|
||||
pointer.set_grab(state, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
|
||||
state.niri.pick_window = Some(tx);
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
|
||||
// Redraw to update the cursor.
|
||||
state.niri.queue_redraw_all();
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let id = result.map_err(|_| String::from("error getting picked window info"))?;
|
||||
let window = id.and_then(|id| {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
state.windows.windows.get(&id.get()).cloned()
|
||||
});
|
||||
Response::PickedWindow(window)
|
||||
}
|
||||
Request::PickColor => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
state.handle_pick_color(tx);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let color = result.map_err(|_| String::from("error getting picked color"))?;
|
||||
Response::PickedColor(color)
|
||||
}
|
||||
Request::Action(action) => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
|
||||
|
||||
+52
-17
@@ -1,16 +1,17 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use niri_config::layer_rule::LayerRule;
|
||||
use niri_config::Config;
|
||||
use smithay::backend::renderer::element::surface::{
|
||||
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::desktop::{LayerSurface, PopupManager};
|
||||
use smithay::utils::{Logical, Rectangle, Scale};
|
||||
use smithay::utils::{Logical, Point, Scale, Size};
|
||||
|
||||
use super::ResolvedLayerRules;
|
||||
use crate::layout::shadow::Shadow;
|
||||
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, SplitElements};
|
||||
|
||||
@@ -23,25 +24,59 @@ pub struct MappedLayer {
|
||||
rules: ResolvedLayerRules,
|
||||
|
||||
/// Buffer to draw instead of the surface when it should be blocked out.
|
||||
block_out_buffer: RefCell<SolidColorBuffer>,
|
||||
block_out_buffer: SolidColorBuffer,
|
||||
|
||||
/// The shadow around the surface.
|
||||
shadow: Shadow,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
LayerSurfaceRenderElement<R> => {
|
||||
Wayland = WaylandSurfaceRenderElement<R>,
|
||||
SolidColor = SolidColorRenderElement,
|
||||
Shadow = ShadowRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl MappedLayer {
|
||||
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules) -> Self {
|
||||
pub fn new(surface: LayerSurface, rules: ResolvedLayerRules, config: &Config) -> Self {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
let shadow_config = rules.shadow.resolve_against(shadow_config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
rules,
|
||||
block_out_buffer: RefCell::new(SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.])),
|
||||
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
|
||||
shadow: Shadow::new(shadow_config),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &Config) {
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
let shadow_config = self.rules.shadow.resolve_against(shadow_config);
|
||||
self.shadow.update_config(shadow_config);
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, size: Size<f64, Logical>, scale: Scale<f64>) {
|
||||
// Round to physical pixels.
|
||||
let size = size.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
self.block_out_buffer.resize(size);
|
||||
|
||||
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
|
||||
// FIXME: is_active based on keyboard focus?
|
||||
self.shadow
|
||||
.update_render_elements(size, true, radius, scale.x, 1.);
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> &LayerSurface {
|
||||
&self.surface
|
||||
}
|
||||
@@ -64,7 +99,7 @@ impl MappedLayer {
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> SplitElements<LayerSurfaceRenderElement<R>> {
|
||||
@@ -74,23 +109,19 @@ impl MappedLayer {
|
||||
|
||||
if target.should_block_out(self.rules.block_out_from) {
|
||||
// Round to physical pixels.
|
||||
let geometry = geometry
|
||||
.to_f64()
|
||||
.to_physical_precise_round(scale)
|
||||
.to_logical(scale);
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
let mut buffer = self.block_out_buffer.borrow_mut();
|
||||
buffer.resize(geometry.size.to_f64());
|
||||
// FIXME: take geometry-corner-radius into account.
|
||||
let elem = SolidColorRenderElement::from_buffer(
|
||||
&buffer,
|
||||
geometry.loc,
|
||||
&self.block_out_buffer,
|
||||
location,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
rv.normal.push(elem.into());
|
||||
} else {
|
||||
// Layer surfaces don't have extra geometry like windows.
|
||||
let buf_pos = geometry.loc;
|
||||
let buf_pos = location;
|
||||
|
||||
let surface = self.surface.wl_surface();
|
||||
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||
@@ -100,7 +131,7 @@ impl MappedLayer {
|
||||
rv.popups.extend(render_elements_from_surface_tree(
|
||||
renderer,
|
||||
popup.wl_surface(),
|
||||
(buf_pos + offset).to_physical_precise_round(scale),
|
||||
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
@@ -117,6 +148,10 @@ impl MappedLayer {
|
||||
);
|
||||
}
|
||||
|
||||
let location = location.to_physical_precise_round(scale).to_logical(scale);
|
||||
rv.normal
|
||||
.extend(self.shadow.render(renderer, location).map(Into::into));
|
||||
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -1,5 +1,5 @@
|
||||
use niri_config::layer_rule::{LayerRule, Match};
|
||||
use niri_config::BlockOutFrom;
|
||||
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
|
||||
use smithay::desktop::LayerSurface;
|
||||
|
||||
pub mod mapped;
|
||||
@@ -8,10 +8,17 @@ pub use mapped::MappedLayer;
|
||||
/// Rules fully resolved for a layer-shell surface.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ResolvedLayerRules {
|
||||
/// Extra opacity to draw this window with.
|
||||
/// Extra opacity to draw this layer surface with.
|
||||
pub opacity: Option<f32>,
|
||||
/// Whether to block out this window from certain render targets.
|
||||
|
||||
/// Whether to block out this layer surface from certain render targets.
|
||||
pub block_out_from: Option<BlockOutFrom>,
|
||||
|
||||
/// Shadow overrides.
|
||||
pub shadow: ShadowRule,
|
||||
|
||||
/// Corner radius to assume this layer surface has.
|
||||
pub geometry_corner_radius: Option<CornerRadius>,
|
||||
}
|
||||
|
||||
impl ResolvedLayerRules {
|
||||
@@ -19,6 +26,17 @@ impl ResolvedLayerRules {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +70,11 @@ impl ResolvedLayerRules {
|
||||
if let Some(x) = rule.block_out_from {
|
||||
resolved.block_out_from = Some(x);
|
||||
}
|
||||
if let Some(x) = rule.geometry_corner_radius {
|
||||
resolved.geometry_corner_radius = Some(x);
|
||||
}
|
||||
|
||||
resolved.shadow.merge_with(&rule.shadow);
|
||||
}
|
||||
|
||||
resolved
|
||||
|
||||
+87
-58
@@ -26,7 +26,7 @@ use crate::utils::{
|
||||
use crate::window::ResolvedWindowRules;
|
||||
|
||||
/// By how many logical pixels the directional move commands move floating windows.
|
||||
const DIRECTIONAL_MOVE_PX: f64 = 50.;
|
||||
pub const DIRECTIONAL_MOVE_PX: f64 = 50.;
|
||||
|
||||
/// Space for floating windows.
|
||||
#[derive(Debug)]
|
||||
@@ -259,7 +259,16 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
self.tiles.iter().any(Tile::are_animations_ongoing) || !self.closing_windows.is_empty()
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
|
||||
pub fn are_transitions_ongoing(&self) -> bool {
|
||||
self.tiles.iter().any(Tile::are_transitions_ongoing) || !self.closing_windows.is_empty()
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
is_active: bool,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
extra_scale: f64,
|
||||
) {
|
||||
let active = self.active_window_id.clone();
|
||||
for (tile, offset) in self.tiles_with_offsets_mut() {
|
||||
let id = tile.window().id();
|
||||
@@ -267,7 +276,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
|
||||
let mut tile_view_rect = view_rect;
|
||||
tile_view_rect.loc -= offset + tile.render_offset();
|
||||
tile.update(is_active, tile_view_rect);
|
||||
tile.update_render_elements(is_active, tile_view_rect, extra_scale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +327,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
|
||||
pub fn new_window_toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
|
||||
let border_config = rules.border.resolve_against(self.options.border);
|
||||
compute_toplevel_bounds(border_config, self.working_area.size)
|
||||
}
|
||||
@@ -365,6 +374,14 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
.map(Tile::window)
|
||||
}
|
||||
|
||||
pub fn active_window_mut(&mut self) -> Option<&mut W> {
|
||||
let id = self.active_window_id.as_ref()?;
|
||||
self.tiles
|
||||
.iter_mut()
|
||||
.find(|tile| tile.window().id() == id)
|
||||
.map(Tile::window_mut)
|
||||
}
|
||||
|
||||
pub fn has_window(&self, id: &W::Id) -> bool {
|
||||
self.tiles.iter().any(|tile| tile.window().id() == id)
|
||||
}
|
||||
@@ -615,7 +632,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
.preset_column_widths
|
||||
.iter()
|
||||
.position(|preset| {
|
||||
let resolved = preset.resolve_no_gaps(&self.options, available_size);
|
||||
let resolved = resolve_preset_size(*preset, available_size);
|
||||
match resolved {
|
||||
// Some allowance for fractional scaling purposes.
|
||||
ResolvedSize::Tile(resolved) => current_tile + 1. < resolved,
|
||||
@@ -626,19 +643,22 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
};
|
||||
|
||||
let preset = self.options.preset_column_widths[preset_idx];
|
||||
let change = match preset {
|
||||
ColumnWidth::Proportion(prop) => SizeChange::SetProportion(prop * 100.),
|
||||
ColumnWidth::Fixed(fixed) => SizeChange::SetFixed(fixed.round() as i32),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.set_window_width(Some(&id), change, true);
|
||||
self.set_window_width(Some(&id), SizeChange::from(preset), true);
|
||||
|
||||
self.tiles[idx].floating_preset_width_idx = Some(preset_idx);
|
||||
|
||||
self.interactive_resize_end(Some(&id));
|
||||
}
|
||||
|
||||
pub fn start_open_animation(&mut self, id: &W::Id) -> bool {
|
||||
let Some(idx) = self.idx_of(id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.tiles[idx].start_open_animation();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn toggle_window_height(&mut self, id: Option<&W::Id>) {
|
||||
let Some(id) = id.or(self.active_window_id.as_ref()).cloned() else {
|
||||
return;
|
||||
@@ -669,12 +689,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
};
|
||||
|
||||
let preset = self.options.preset_window_heights[preset_idx];
|
||||
let change = match preset {
|
||||
PresetSize::Proportion(prop) => SizeChange::SetProportion(prop * 100.),
|
||||
PresetSize::Fixed(fixed) => SizeChange::SetFixed(fixed),
|
||||
};
|
||||
|
||||
self.set_window_height(Some(&id), change, true);
|
||||
self.set_window_height(Some(&id), SizeChange::from(preset), true);
|
||||
|
||||
let tile = &mut self.tiles[idx];
|
||||
tile.floating_preset_height_idx = Some(preset_idx);
|
||||
@@ -836,6 +851,26 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_topmost(&mut self) {
|
||||
let result = self
|
||||
.tiles_with_offsets()
|
||||
.min_by(|(_, pos_a), (_, pos_b)| f64::total_cmp(&pos_a.y, &pos_b.y));
|
||||
if let Some((tile, _)) = result {
|
||||
let id = tile.window().id().clone();
|
||||
self.activate_window(&id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_bottommost(&mut self) {
|
||||
let result = self
|
||||
.tiles_with_offsets()
|
||||
.max_by(|(_, pos_a), (_, pos_b)| f64::total_cmp(&pos_a.y, &pos_b.y));
|
||||
if let Some((tile, _)) = result {
|
||||
let id = tile.window().id().clone();
|
||||
self.activate_window(&id);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_to(&mut self, idx: usize, new_pos: Point<f64, Logical>, animate: bool) {
|
||||
if animate {
|
||||
self.move_and_animate(idx, new_pos);
|
||||
@@ -955,12 +990,13 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
) -> Vec<FloatingSpaceRenderElement<R>> {
|
||||
let mut rv = Vec::new();
|
||||
|
||||
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.
|
||||
@@ -975,7 +1011,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref();
|
||||
|
||||
rv.extend(
|
||||
tile.render(renderer, tile_pos, scale, focus_ring, target)
|
||||
tile.render(renderer, tile_pos, focus_ring, target)
|
||||
.map(Into::into),
|
||||
);
|
||||
}
|
||||
@@ -1135,53 +1171,34 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_width(&self, width: ColumnWidth) -> ResolvedSize {
|
||||
width.resolve_no_gaps(&self.options, self.working_area.size.w)
|
||||
}
|
||||
|
||||
pub fn resolve_height(&self, height: PresetSize) -> ResolvedSize {
|
||||
resolve_preset_size(height, self.working_area.size.h)
|
||||
}
|
||||
|
||||
pub fn new_window_size(
|
||||
&self,
|
||||
width: Option<ColumnWidth>,
|
||||
width: Option<PresetSize>,
|
||||
height: Option<PresetSize>,
|
||||
rules: &ResolvedWindowRules,
|
||||
) -> Size<i32, Logical> {
|
||||
let border = rules.border.resolve_against(self.options.border);
|
||||
|
||||
let width = if let Some(width) = width {
|
||||
let width = match self.resolve_width(width) {
|
||||
ResolvedSize::Tile(mut size) => {
|
||||
if !border.off {
|
||||
size -= border.width.0 * 2.;
|
||||
let resolve = |size: Option<PresetSize>, working_area_size: f64| {
|
||||
if let Some(size) = size {
|
||||
let size = match resolve_preset_size(size, working_area_size) {
|
||||
ResolvedSize::Tile(mut size) => {
|
||||
if !border.off {
|
||||
size -= border.width.0 * 2.;
|
||||
}
|
||||
size
|
||||
}
|
||||
size
|
||||
}
|
||||
ResolvedSize::Window(size) => size,
|
||||
};
|
||||
ResolvedSize::Window(size) => size,
|
||||
};
|
||||
|
||||
max(1, width.floor() as i32)
|
||||
} else {
|
||||
0
|
||||
max(1, size.floor() as i32)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let height = if let Some(height) = height {
|
||||
let height = match self.resolve_height(height) {
|
||||
ResolvedSize::Tile(mut size) => {
|
||||
if !border.off {
|
||||
size -= border.width.0 * 2.;
|
||||
}
|
||||
size
|
||||
}
|
||||
ResolvedSize::Window(size) => size,
|
||||
};
|
||||
|
||||
max(1, height.floor() as i32)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let width = resolve(width, self.working_area.size.w);
|
||||
let height = resolve(height, self.working_area.size.h);
|
||||
|
||||
Size::from((width, height))
|
||||
}
|
||||
@@ -1195,12 +1212,24 @@ impl<W: LayoutElement> FloatingSpace<W> {
|
||||
let area = self.working_area;
|
||||
|
||||
let mut pos = Point::from((pos.x.0, pos.y.0));
|
||||
if relative_to == RelativeTo::TopRight || relative_to == RelativeTo::BottomRight {
|
||||
if relative_to == RelativeTo::TopRight
|
||||
|| relative_to == RelativeTo::BottomRight
|
||||
|| relative_to == RelativeTo::Right
|
||||
{
|
||||
pos.x = area.size.w - size.w - pos.x;
|
||||
}
|
||||
if relative_to == RelativeTo::BottomLeft || relative_to == RelativeTo::BottomRight {
|
||||
if relative_to == RelativeTo::BottomLeft
|
||||
|| relative_to == RelativeTo::BottomRight
|
||||
|| relative_to == RelativeTo::Bottom
|
||||
{
|
||||
pos.y = area.size.h - size.h - pos.y;
|
||||
}
|
||||
if relative_to == RelativeTo::Top || relative_to == RelativeTo::Bottom {
|
||||
pos.x += area.size.w / 2.0 - size.w / 2.0
|
||||
}
|
||||
if relative_to == RelativeTo::Left || relative_to == RelativeTo::Right {
|
||||
pos.y += area.size.h / 2.0 - size.h / 2.0
|
||||
}
|
||||
|
||||
pos + self.working_area.loc
|
||||
})
|
||||
|
||||
+18
-11
@@ -1,14 +1,15 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::utils::round_logical_in_physical_max1;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FocusRing {
|
||||
@@ -53,6 +54,7 @@ impl FocusRing {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
win_size: Size<f64, Logical>,
|
||||
@@ -61,8 +63,11 @@ impl FocusRing {
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
alpha: f32,
|
||||
) {
|
||||
let width = self.config.width.0;
|
||||
// let scale = scale * extra_visual_scale;
|
||||
// let width = self.config.width.0 / extra_visual_scale;
|
||||
let width = round_logical_in_physical_max1(scale, self.config.width.0);
|
||||
self.full_size = win_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
@@ -86,13 +91,7 @@ impl FocusRing {
|
||||
self.use_border_shader = radius != CornerRadius::default() || gradient.is_some();
|
||||
|
||||
// Set the defaults for solid color + rounded corners.
|
||||
let gradient = gradient.unwrap_or(Gradient {
|
||||
from: color,
|
||||
to: color,
|
||||
angle: 0,
|
||||
relative_to: GradientRelativeTo::Window,
|
||||
in_: GradientInterpolation::default(),
|
||||
});
|
||||
let gradient = gradient.unwrap_or_else(|| Gradient::from(color));
|
||||
|
||||
let full_rect = Rectangle::new(Point::from((-width, -width)), self.full_size);
|
||||
let gradient_area = match gradient.relative_to {
|
||||
@@ -187,6 +186,7 @@ impl FocusRing {
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -205,6 +205,7 @@ impl FocusRing {
|
||||
rounded_corner_border_width,
|
||||
radius,
|
||||
scale as f32,
|
||||
alpha,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,7 +236,9 @@ impl FocusRing {
|
||||
let elem = if self.use_border_shader && has_border_shader {
|
||||
border.clone().with_location(location).into()
|
||||
} else {
|
||||
SolidColorRenderElement::from_buffer(buffer, location, 1., Kind::Unspecified).into()
|
||||
let alpha = border.alpha();
|
||||
SolidColorRenderElement::from_buffer(buffer, location, alpha, Kind::Unspecified)
|
||||
.into()
|
||||
};
|
||||
rv.push(elem);
|
||||
};
|
||||
@@ -262,4 +265,8 @@ impl FocusRing {
|
||||
pub fn is_off(&self) -> bool {
|
||||
self.config.off
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &niri_config::FocusRing {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ impl InsertHintElement {
|
||||
scale: f64,
|
||||
) {
|
||||
self.inner
|
||||
.update_render_elements(size, true, false, view_rect, radius, scale);
|
||||
.update_render_elements(size, true, false, view_rect, radius, scale, 1.);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
|
||||
+1320
-3255
File diff suppressed because it is too large
Load Diff
+625
-247
File diff suppressed because it is too large
Load Diff
@@ -2,32 +2,30 @@ use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use glam::{Mat3, Vec2};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::element::{Element as _, Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::render_to_encompassing_texture;
|
||||
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenData, OffscreenRenderElement};
|
||||
use crate::render_helpers::shader_element::ShaderRenderElement;
|
||||
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpenAnimation {
|
||||
anim: Animation,
|
||||
random_seed: f32,
|
||||
buffer: OffscreenBuffer,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
OpeningWindowRenderElement => {
|
||||
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
||||
Offscreen = RelocateRenderElement<RescaleRenderElement<OffscreenRenderElement>>,
|
||||
Shader = ShaderRenderElement,
|
||||
}
|
||||
}
|
||||
@@ -37,6 +35,7 @@ impl OpenAnimation {
|
||||
Self {
|
||||
anim,
|
||||
random_seed: fastrand::f32(),
|
||||
buffer: OffscreenBuffer::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,23 +54,23 @@ impl OpenAnimation {
|
||||
geo_size: Size<f64, Logical>,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
) -> anyhow::Result<OpeningWindowRenderElement> {
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<(OpeningWindowRenderElement, OffscreenData)> {
|
||||
let progress = self.anim.value();
|
||||
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
let (texture, _sync_point, geo) = render_to_encompassing_texture(
|
||||
renderer,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements,
|
||||
)
|
||||
.context("error rendering to texture")?;
|
||||
|
||||
let offset = geo.loc.to_f64().to_logical(scale);
|
||||
let texture_size = geo.size.to_f64().to_logical(scale);
|
||||
let (elem, _sync_point, mut data) = self
|
||||
.buffer
|
||||
.render(renderer, scale, elements)
|
||||
.context("error rendering to offscreen buffer")?;
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Open).is_some() {
|
||||
// OffscreenBuffer renders with Transform::Normal and the scale that we passed, so we
|
||||
// can assume that below.
|
||||
let offset = elem.offset();
|
||||
let texture = elem.texture();
|
||||
let texture_size = elem.logical_size();
|
||||
|
||||
let mut area = Rectangle::new(location + offset, texture_size);
|
||||
|
||||
// Expand the area a bit to allow for more varied effects.
|
||||
@@ -99,12 +98,12 @@ impl OpenAnimation {
|
||||
let geo_to_tex =
|
||||
Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size);
|
||||
|
||||
return Ok(ShaderRenderElement::new(
|
||||
let elem = ShaderRenderElement::new(
|
||||
ProgramType::Open,
|
||||
area.size,
|
||||
None,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
alpha,
|
||||
vec![
|
||||
mat3_uniform("niri_input_to_geo", input_to_geo),
|
||||
Uniform::new("niri_geo_size", geo_size.to_array()),
|
||||
@@ -116,36 +115,29 @@ impl OpenAnimation {
|
||||
HashMap::from([(String::from("niri_tex"), texture.clone())]),
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.with_location(area.loc)
|
||||
.into());
|
||||
.with_location(area.loc);
|
||||
|
||||
// We're drawing the shader, not the offscreen itself.
|
||||
data.id = elem.id().clone();
|
||||
|
||||
return Ok((elem.into(), data));
|
||||
}
|
||||
|
||||
let buffer =
|
||||
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, Vec::new());
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer,
|
||||
Point::from((0., 0.)),
|
||||
clamped_progress as f32,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
let elem = elem.with_alpha(clamped_progress as f32 * alpha);
|
||||
|
||||
let center = geo_size.to_point().downscale(2.);
|
||||
let elem = RescaleRenderElement::from_element(
|
||||
elem,
|
||||
(center - offset).to_physical_precise_round(scale),
|
||||
center.to_physical_precise_round(scale),
|
||||
(progress / 2. + 0.5).max(0.),
|
||||
);
|
||||
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
(location + offset).to_physical_precise_round(scale),
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
Ok(elem.into())
|
||||
Ok((elem.into(), data))
|
||||
}
|
||||
}
|
||||
|
||||
+1301
-275
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Shadow {
|
||||
shader_rects: Vec<Rectangle<f64, Logical>>,
|
||||
shaders: Vec<ShadowRenderElement>,
|
||||
config: niri_config::Shadow,
|
||||
}
|
||||
|
||||
impl Shadow {
|
||||
pub fn new(config: niri_config::Shadow) -> Self {
|
||||
Self {
|
||||
shader_rects: Vec::new(),
|
||||
shaders: Vec::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::Shadow) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for elem in &mut self.shaders {
|
||||
elem.damage_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
win_size: Size<f64, Logical>,
|
||||
is_active: bool,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
alpha: f32,
|
||||
) {
|
||||
let ceil = |logical: f64| (logical * scale).ceil() / scale;
|
||||
|
||||
// All of this stuff should end up aligned to physical pixels because:
|
||||
// * Window size is rounded to physical pixels before being passed to this function.
|
||||
// * We will ceil the corner radii below.
|
||||
// * We do not divide anything, only add, subtract and multiply by integers.
|
||||
// * At rendering time, tile positions are rounded to physical pixels.
|
||||
|
||||
let width = self.config.softness.0;
|
||||
// Like in CSS box-shadow.
|
||||
let sigma = width / 2.;
|
||||
// Adjust width to draw all necessary pixels.
|
||||
let width = ceil(sigma * 3.);
|
||||
|
||||
let offset = self.config.offset;
|
||||
let offset = Point::from((ceil(offset.x.0), ceil(offset.y.0)));
|
||||
|
||||
let spread = self.config.spread.0;
|
||||
let spread = ceil(spread.abs()).copysign(spread);
|
||||
let offset = offset - Point::from((spread, spread));
|
||||
|
||||
let win_radius = radius.fit_to(win_size.w as f32, win_size.h as f32);
|
||||
|
||||
let box_size = if spread >= 0. {
|
||||
win_size + Size::from((spread, spread)).upscale(2.)
|
||||
} else {
|
||||
// This is a saturating sub.
|
||||
win_size - Size::from((-spread, -spread)).upscale(2.)
|
||||
};
|
||||
let radius = win_radius.expanded_by(spread as f32);
|
||||
|
||||
let shader_size = box_size + Size::from((width, width)).upscale(2.);
|
||||
|
||||
let color = if is_active {
|
||||
self.config.color
|
||||
} else {
|
||||
// Default to slightly more transparent.
|
||||
self.config
|
||||
.inactive_color
|
||||
.unwrap_or(self.config.color * 0.75)
|
||||
};
|
||||
|
||||
let shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size);
|
||||
|
||||
// This is actually offset relative to shader_geo, this is handled below.
|
||||
let window_geo = Rectangle::new(Point::from((0., 0.)), win_size);
|
||||
|
||||
if !self.config.draw_behind_window {
|
||||
let top_left = ceil(f64::from(win_radius.top_left));
|
||||
let top_right = f64::min(win_size.w - top_left, ceil(f64::from(win_radius.top_right)));
|
||||
let bottom_left = f64::min(
|
||||
win_size.h - top_left,
|
||||
ceil(f64::from(win_radius.bottom_left)),
|
||||
);
|
||||
let bottom_right = f64::min(
|
||||
win_size.h - top_right,
|
||||
f64::min(
|
||||
win_size.w - bottom_left,
|
||||
ceil(f64::from(win_radius.bottom_right)),
|
||||
),
|
||||
);
|
||||
|
||||
let top_left = Rectangle::new(Point::from((0., 0.)), Size::from((top_left, top_left)));
|
||||
let top_right = Rectangle::new(
|
||||
Point::from((win_size.w - top_right, 0.)),
|
||||
Size::from((top_right, top_right)),
|
||||
);
|
||||
let bottom_right = Rectangle::new(
|
||||
Point::from((win_size.w - bottom_right, win_size.h - bottom_right)),
|
||||
Size::from((bottom_right, bottom_right)),
|
||||
);
|
||||
let bottom_left = Rectangle::new(
|
||||
Point::from((0., win_size.h - bottom_left)),
|
||||
Size::from((bottom_left, bottom_left)),
|
||||
);
|
||||
|
||||
let mut background =
|
||||
window_geo.subtract_rects([top_left, top_right, bottom_right, bottom_left]);
|
||||
for rect in &mut background {
|
||||
rect.loc -= offset;
|
||||
}
|
||||
|
||||
self.shader_rects = shader_geo.subtract_rects(background);
|
||||
self.shaders
|
||||
.resize_with(self.shader_rects.len(), Default::default);
|
||||
|
||||
for (shader, rect) in zip(&mut self.shaders, &mut self.shader_rects) {
|
||||
shader.update(
|
||||
rect.size,
|
||||
Rectangle::new(rect.loc.upscale(-1.), box_size),
|
||||
color,
|
||||
sigma as f32,
|
||||
radius,
|
||||
scale as f32,
|
||||
Rectangle::new(window_geo.loc - offset - rect.loc, window_geo.size),
|
||||
win_radius,
|
||||
alpha,
|
||||
);
|
||||
|
||||
rect.loc += offset;
|
||||
}
|
||||
} else {
|
||||
self.shader_rects.resize_with(1, Default::default);
|
||||
self.shader_rects[0] = shader_geo;
|
||||
|
||||
self.shaders.resize_with(1, Default::default);
|
||||
self.shaders[0].update(
|
||||
shader_geo.size,
|
||||
Rectangle::new(shader_geo.loc.upscale(-1.), box_size),
|
||||
color,
|
||||
sigma as f32,
|
||||
radius,
|
||||
scale as f32,
|
||||
Rectangle::zero(),
|
||||
Default::default(),
|
||||
alpha,
|
||||
);
|
||||
|
||||
self.shader_rects[0].loc += offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
if !self.config.on {
|
||||
return None.into_iter().flatten();
|
||||
}
|
||||
|
||||
let has_shadow_shader = ShadowRenderElement::has_shader(renderer);
|
||||
if !has_shadow_shader {
|
||||
return None.into_iter().flatten();
|
||||
}
|
||||
|
||||
let rv = zip(&self.shaders, &self.shader_rects)
|
||||
.map(move |(shader, rect)| shader.clone().with_location(location + rect.loc));
|
||||
|
||||
Some(rv).into_iter().flatten()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use super::tile::Tile;
|
||||
use super::LayoutElement;
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::utils::{
|
||||
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TabIndicator {
|
||||
shader_locs: Vec<Point<f64, Logical>>,
|
||||
shaders: Vec<BorderRenderElement>,
|
||||
open_anim: Option<Animation>,
|
||||
config: niri_config::TabIndicator,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TabInfo {
|
||||
/// Gradient for the tab indicator.
|
||||
pub gradient: Gradient,
|
||||
/// Tab geometry in the same coordinate system as the area.
|
||||
pub geometry: Rectangle<f64, Logical>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
TabIndicatorRenderElement => {
|
||||
Gradient = BorderRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
impl TabIndicator {
|
||||
pub fn new(config: niri_config::TabIndicator) -> Self {
|
||||
Self {
|
||||
shader_locs: Vec::new(),
|
||||
shaders: Vec::new(),
|
||||
open_anim: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::TabIndicator) {
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
for elem in &mut self.shaders {
|
||||
elem.damage_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self) {
|
||||
if let Some(anim) = &mut self.open_anim {
|
||||
if anim.is_done() {
|
||||
self.open_anim = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.open_anim.is_some()
|
||||
}
|
||||
|
||||
pub fn start_open_animation(&mut self, clock: Clock, config: niri_config::Animation) {
|
||||
self.open_anim = Some(Animation::new(clock, 0., 1., 0., config));
|
||||
}
|
||||
|
||||
fn tab_rects(
|
||||
&self,
|
||||
area: Rectangle<f64, Logical>,
|
||||
count: usize,
|
||||
scale: f64,
|
||||
) -> impl Iterator<Item = Rectangle<f64, Logical>> {
|
||||
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||
let round_max1 = |logical: f64| round_logical_in_physical_max1(scale, logical);
|
||||
|
||||
let progress = self.open_anim.as_ref().map_or(1., |a| a.value().max(0.));
|
||||
|
||||
let width = round_max1(self.config.width.0);
|
||||
let gap = round_max1(self.config.gap.0);
|
||||
let gaps_between = round_max1(self.config.gaps_between_tabs.0);
|
||||
|
||||
let position = self.config.position;
|
||||
let side = match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => area.size.h,
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => area.size.w,
|
||||
};
|
||||
let total_prop = self.config.length.total_proportion.unwrap_or(0.5);
|
||||
let min_length = round(side * total_prop.clamp(0., 2.));
|
||||
|
||||
// Compute px_per_tab before applying the animation to gaps_between in order to avoid it
|
||||
// growing and shrinking over the duration of the animation.
|
||||
let pixel = 1. / scale;
|
||||
let shortest_length = count as f64 * (pixel + gaps_between) - gaps_between;
|
||||
let length = f64::max(min_length, shortest_length);
|
||||
let px_per_tab = (length + gaps_between) / count as f64 - gaps_between;
|
||||
|
||||
let px_per_tab = px_per_tab * progress;
|
||||
let gaps_between = round(self.config.gaps_between_tabs.0 * progress);
|
||||
|
||||
let length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
|
||||
let px_per_tab = floor_logical_in_physical_max1(scale, px_per_tab);
|
||||
let floored_length = count as f64 * (px_per_tab + gaps_between) - gaps_between;
|
||||
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
|
||||
|
||||
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
|
||||
match position {
|
||||
TabIndicatorPosition::Left => (),
|
||||
TabIndicatorPosition::Right => shader_loc.x = area.size.w + gap,
|
||||
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
|
||||
TabIndicatorPosition::Bottom => {
|
||||
shader_loc.x = shader_loc.y;
|
||||
shader_loc.y = area.size.h + gap;
|
||||
}
|
||||
}
|
||||
shader_loc += area.loc;
|
||||
|
||||
(0..count).map(move |_| {
|
||||
let mut px_per_tab = px_per_tab;
|
||||
if ones_left > 0 {
|
||||
ones_left -= 1;
|
||||
px_per_tab += pixel;
|
||||
}
|
||||
|
||||
let loc = shader_loc;
|
||||
|
||||
match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
|
||||
shader_loc.y += px_per_tab + gaps_between
|
||||
}
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
|
||||
shader_loc.x += px_per_tab + gaps_between
|
||||
}
|
||||
}
|
||||
|
||||
let size = match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => {
|
||||
Size::from((width, px_per_tab))
|
||||
}
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => {
|
||||
Size::from((px_per_tab, width))
|
||||
}
|
||||
};
|
||||
|
||||
Rectangle::new(loc, size)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
enabled: bool,
|
||||
// Geometry of the tabs area.
|
||||
area: Rectangle<f64, Logical>,
|
||||
// View rect relative to the tabs area.
|
||||
area_view_rect: Rectangle<f64, Logical>,
|
||||
// Tab count, should match the tabs iterator length.
|
||||
tab_count: usize,
|
||||
tabs: impl Iterator<Item = TabInfo>,
|
||||
is_active: bool,
|
||||
scale: f64,
|
||||
) {
|
||||
if !enabled || self.config.off {
|
||||
self.shader_locs.clear();
|
||||
self.shaders.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
let count = tab_count;
|
||||
if self.config.hide_when_single_tab && count == 1 {
|
||||
self.shader_locs.clear();
|
||||
self.shaders.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
self.shaders.resize_with(count, Default::default);
|
||||
self.shader_locs.resize_with(count, Default::default);
|
||||
|
||||
let position = self.config.position;
|
||||
let radius = self.config.corner_radius.0 as f32;
|
||||
let shared_rounded_corners = self.config.gaps_between_tabs.0 == 0.;
|
||||
let mut tabs_left = tab_count;
|
||||
|
||||
let rects = self.tab_rects(area, count, scale);
|
||||
for ((shader, loc), (tab, rect)) in zip(
|
||||
zip(&mut self.shaders, &mut self.shader_locs),
|
||||
zip(tabs, rects),
|
||||
) {
|
||||
*loc = rect.loc;
|
||||
|
||||
let mut gradient_area = match tab.gradient.relative_to {
|
||||
GradientRelativeTo::Window => tab.geometry,
|
||||
GradientRelativeTo::WorkspaceView => area_view_rect,
|
||||
};
|
||||
gradient_area.loc -= *loc;
|
||||
|
||||
let mut color_from = tab.gradient.from;
|
||||
let mut color_to = tab.gradient.to;
|
||||
if !is_active {
|
||||
color_from *= 0.5;
|
||||
color_to *= 0.5;
|
||||
}
|
||||
|
||||
let radius = if shared_rounded_corners && tab_count > 1 {
|
||||
if tabs_left == tab_count {
|
||||
// First tab.
|
||||
match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
|
||||
top_left: radius,
|
||||
top_right: radius,
|
||||
bottom_right: 0.,
|
||||
bottom_left: 0.,
|
||||
},
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
|
||||
top_left: radius,
|
||||
top_right: 0.,
|
||||
bottom_right: 0.,
|
||||
bottom_left: radius,
|
||||
},
|
||||
}
|
||||
} else if tabs_left == 1 {
|
||||
// Last tab.
|
||||
match position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => CornerRadius {
|
||||
top_left: 0.,
|
||||
top_right: 0.,
|
||||
bottom_right: radius,
|
||||
bottom_left: radius,
|
||||
},
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => CornerRadius {
|
||||
top_left: 0.,
|
||||
top_right: radius,
|
||||
bottom_right: radius,
|
||||
bottom_left: 0.,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Tab in the middle.
|
||||
CornerRadius::default()
|
||||
}
|
||||
} else {
|
||||
// Separate tabs, or the only tab.
|
||||
CornerRadius::from(radius)
|
||||
};
|
||||
let radius = radius.fit_to(rect.size.w as f32, rect.size.h as f32);
|
||||
tabs_left -= 1;
|
||||
|
||||
shader.update(
|
||||
rect.size,
|
||||
gradient_area,
|
||||
tab.gradient.in_,
|
||||
color_from,
|
||||
color_to,
|
||||
((tab.gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::from_size(rect.size),
|
||||
0.,
|
||||
radius,
|
||||
scale as f32,
|
||||
1.,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hit(
|
||||
&self,
|
||||
area: Rectangle<f64, Logical>,
|
||||
tab_count: usize,
|
||||
scale: f64,
|
||||
point: Point<f64, Logical>,
|
||||
) -> Option<usize> {
|
||||
if self.config.off {
|
||||
return None;
|
||||
}
|
||||
|
||||
let count = tab_count;
|
||||
if self.config.hide_when_single_tab && count == 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.tab_rects(area, count, scale)
|
||||
.enumerate()
|
||||
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
pos: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
|
||||
let has_border_shader = BorderRenderElement::has_shader(renderer);
|
||||
if !has_border_shader {
|
||||
return None.into_iter().flatten();
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// Extra size occupied by the tab indicator.
|
||||
pub fn extra_size(&self, tab_count: usize, scale: f64) -> Size<f64, Logical> {
|
||||
if self.config.off
|
||||
|| !self.config.place_within_column
|
||||
|| (self.config.hide_when_single_tab && tab_count == 1)
|
||||
{
|
||||
return Size::from((0., 0.));
|
||||
}
|
||||
|
||||
let round = |logical: f64| round_logical_in_physical(scale, logical);
|
||||
let width = round(self.config.width.0);
|
||||
let gap = round(self.config.gap.0);
|
||||
|
||||
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
|
||||
// that it peeks from the other side of the window".
|
||||
let size = f64::max(0., width + gap);
|
||||
|
||||
match self.config.position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Right => Size::from((size, 0.)),
|
||||
TabIndicatorPosition::Top | TabIndicatorPosition::Bottom => Size::from((0., size)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset of the tabbed content due to space occupied by the tab indicator.
|
||||
pub fn content_offset(&self, tab_count: usize, scale: f64) -> Point<f64, Logical> {
|
||||
match self.config.position {
|
||||
TabIndicatorPosition::Left | TabIndicatorPosition::Top => {
|
||||
self.extra_size(tab_count, scale).to_point()
|
||||
}
|
||||
TabIndicatorPosition::Right | TabIndicatorPosition::Bottom => Point::from((0., 0.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> niri_config::TabIndicator {
|
||||
self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl TabInfo {
|
||||
pub fn from_tile<W: LayoutElement>(
|
||||
tile: &Tile<W>,
|
||||
position: Point<f64, Logical>,
|
||||
is_active: bool,
|
||||
config: &niri_config::TabIndicator,
|
||||
) -> Self {
|
||||
let rules = tile.window().rules();
|
||||
let rule = rules.tab_indicator;
|
||||
|
||||
let gradient_from_rule = || {
|
||||
let (color, gradient) = if is_active {
|
||||
(rule.active_color, rule.active_gradient)
|
||||
} else {
|
||||
(rule.inactive_color, rule.inactive_gradient)
|
||||
};
|
||||
let color = color.map(Gradient::from);
|
||||
gradient.or(color)
|
||||
};
|
||||
|
||||
let gradient_from_config = || {
|
||||
let (color, gradient) = if is_active {
|
||||
(config.active_color, config.active_gradient)
|
||||
} else {
|
||||
(config.inactive_color, config.inactive_gradient)
|
||||
};
|
||||
let color = color.map(Gradient::from);
|
||||
gradient.or(color)
|
||||
};
|
||||
|
||||
let gradient_from_border = || {
|
||||
// Come up with tab indicator gradient matching the focus ring or the border, whichever
|
||||
// one is enabled.
|
||||
let focus_ring_config = tile.focus_ring().config();
|
||||
let border_config = tile.border().config();
|
||||
let config = if focus_ring_config.off {
|
||||
border_config
|
||||
} else {
|
||||
focus_ring_config
|
||||
};
|
||||
|
||||
let (color, gradient) = if is_active {
|
||||
(config.active_color, config.active_gradient)
|
||||
} else {
|
||||
(config.inactive_color, config.inactive_gradient)
|
||||
};
|
||||
gradient.unwrap_or_else(|| Gradient::from(color))
|
||||
};
|
||||
|
||||
let gradient = gradient_from_rule()
|
||||
.or_else(gradient_from_config)
|
||||
.unwrap_or_else(gradient_from_border);
|
||||
|
||||
let geometry = Rectangle::new(position, tile.animated_tile_size());
|
||||
|
||||
TabInfo { gradient, geometry }
|
||||
}
|
||||
}
|
||||
+3554
File diff suppressed because it is too large
Load Diff
+323
-95
@@ -1,28 +1,32 @@
|
||||
use core::f64;
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::{Element, Kind};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use super::opening_window::{OpenAnimation, OpeningWindowRenderElement};
|
||||
use super::shadow::Shadow;
|
||||
use super::{
|
||||
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options, SizeFrac,
|
||||
RESIZE_ANIMATION_THRESHOLD,
|
||||
HitType, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
|
||||
SizeFrac, RESIZE_ANIMATION_THRESHOLD,
|
||||
};
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::border::BorderRenderElement;
|
||||
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
|
||||
use crate::render_helpers::damage::ExtraDamage;
|
||||
use crate::render_helpers::offscreen::{OffscreenBuffer, OffscreenRenderElement};
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
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::{render_to_encompassing_texture, RenderTarget};
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{round_logical_in_physical, round_logical_in_physical_max1};
|
||||
|
||||
/// Toplevel window with decorations.
|
||||
#[derive(Debug)]
|
||||
@@ -34,11 +38,11 @@ pub struct Tile<W: LayoutElement> {
|
||||
border: FocusRing,
|
||||
|
||||
/// The focus ring around the window.
|
||||
///
|
||||
/// It's supposed to be on the Workspace, but for the sake of a nicer open animation it's
|
||||
/// currently here.
|
||||
focus_ring: FocusRing,
|
||||
|
||||
/// The shadow around the window.
|
||||
shadow: Shadow,
|
||||
|
||||
/// Whether this tile is fullscreen.
|
||||
///
|
||||
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
||||
@@ -82,6 +86,9 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The animation of a tile visually moving vertically.
|
||||
move_y_animation: Option<MoveAnimation>,
|
||||
|
||||
/// The animation of the tile's opacity.
|
||||
pub(super) alpha_animation: Option<AlphaAnimation>,
|
||||
|
||||
/// Offset during the initial interactive move rubberband.
|
||||
pub(super) interactive_move_offset: Point<f64, Logical>,
|
||||
|
||||
@@ -99,6 +106,11 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// Scale of the output the tile is on (and rounds its sizes to).
|
||||
scale: f64,
|
||||
|
||||
/// Extra scale used for rendering.
|
||||
///
|
||||
/// Applied on top of `scale` and used for visuals only (does not affect the layout).
|
||||
extra_overview_scale: f64,
|
||||
|
||||
/// Clock for driving animations.
|
||||
pub(super) clock: Clock,
|
||||
|
||||
@@ -114,7 +126,9 @@ niri_render_elements! {
|
||||
Opening = OpeningWindowRenderElement,
|
||||
Resize = ResizeRenderElement,
|
||||
Border = BorderRenderElement,
|
||||
Shadow = ShadowRenderElement,
|
||||
ClippedSurface = ClippedSurfaceRenderElement<R>,
|
||||
Offscreen = OffscreenRenderElement,
|
||||
ExtraDamage = ExtraDamage,
|
||||
}
|
||||
}
|
||||
@@ -127,6 +141,7 @@ struct ResizeAnimation {
|
||||
anim: Animation,
|
||||
size_from: Size<f64, Logical>,
|
||||
snapshot: LayoutElementRenderSnapshot,
|
||||
offscreen: OffscreenBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -135,6 +150,18 @@ struct MoveAnimation {
|
||||
from: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct AlphaAnimation {
|
||||
pub(super) anim: Animation,
|
||||
/// Whether the animation should persist after it's done.
|
||||
///
|
||||
/// This is used by things like interactive move which need to animate alpha to
|
||||
/// semitransparent, then hold it at semitransparent for a while, until the operation
|
||||
/// completes.
|
||||
pub(super) hold_after_done: bool,
|
||||
offscreen: OffscreenBuffer,
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Tile<W> {
|
||||
pub fn new(
|
||||
window: W,
|
||||
@@ -146,12 +173,14 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let rules = window.rules();
|
||||
let border_config = rules.border.resolve_against(options.border);
|
||||
let focus_ring_config = rules.focus_ring.resolve_against(options.focus_ring.into());
|
||||
let shadow_config = rules.shadow.resolve_against(options.shadow);
|
||||
let is_fullscreen = window.is_fullscreen();
|
||||
|
||||
Self {
|
||||
window,
|
||||
border: FocusRing::new(border_config.into()),
|
||||
focus_ring: FocusRing::new(focus_ring_config.into()),
|
||||
shadow: Shadow::new(shadow_config),
|
||||
is_fullscreen,
|
||||
fullscreen_backdrop: SolidColorBuffer::new(view_size, [0., 0., 0., 1.]),
|
||||
unfullscreen_to_floating: false,
|
||||
@@ -163,11 +192,13 @@ impl<W: LayoutElement> Tile<W> {
|
||||
resize_animation: None,
|
||||
move_x_animation: None,
|
||||
move_y_animation: None,
|
||||
alpha_animation: None,
|
||||
interactive_move_offset: Point::from((0., 0.)),
|
||||
unmap_snapshot: None,
|
||||
rounded_corner_damage: Default::default(),
|
||||
view_size,
|
||||
scale,
|
||||
extra_overview_scale: 1.,
|
||||
clock,
|
||||
options,
|
||||
}
|
||||
@@ -201,19 +232,23 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.resolve_against(self.options.focus_ring.into());
|
||||
self.focus_ring.update_config(focus_ring_config.into());
|
||||
|
||||
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
self.fullscreen_backdrop.resize(view_size);
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.border.update_shaders();
|
||||
self.focus_ring.update_shaders();
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_window(&mut self) {
|
||||
self.is_fullscreen = self.window.is_fullscreen();
|
||||
|
||||
if let Some(animate_from) = self.window.take_animation_snapshot() {
|
||||
let size_from = if let Some(resize) = self.resize_animation.take() {
|
||||
let (size_from, offscreen) = if let Some(resize) = self.resize_animation.take() {
|
||||
// Compute like in animated_window_size(), but using the snapshot geometry (since
|
||||
// the current one is already overwritten).
|
||||
let mut size = animate_from.size;
|
||||
@@ -224,9 +259,10 @@ impl<W: LayoutElement> Tile<W> {
|
||||
size.w = size_from.w + (size.w - size_from.w) * val;
|
||||
size.h = size_from.h + (size.h - size_from.h) * val;
|
||||
|
||||
size
|
||||
// Also try to reuse the existing offscreen buffer if we have one.
|
||||
(size, resize.offscreen)
|
||||
} else {
|
||||
animate_from.size
|
||||
(animate_from.size, OffscreenBuffer::default())
|
||||
};
|
||||
|
||||
let change = self.window.size().to_f64().to_point() - size_from.to_point();
|
||||
@@ -243,6 +279,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
anim,
|
||||
size_from,
|
||||
snapshot: animate_from,
|
||||
offscreen,
|
||||
});
|
||||
} else {
|
||||
self.resize_animation = None;
|
||||
@@ -257,6 +294,9 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.resolve_against(self.options.focus_ring.into());
|
||||
self.focus_ring.update_config(focus_ring_config.into());
|
||||
|
||||
let shadow_config = rules.shadow.resolve_against(self.options.shadow);
|
||||
self.shadow.update_config(shadow_config);
|
||||
|
||||
let window_size = self.window_size();
|
||||
let radius = rules
|
||||
.geometry_corner_radius
|
||||
@@ -289,18 +329,40 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.move_y_animation = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(alpha) = &mut self.alpha_animation {
|
||||
if !alpha.hold_after_done && alpha.anim.is_done() {
|
||||
self.alpha_animation = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
self.are_transitions_ongoing() || self.window.rules().baba_is_float == Some(true)
|
||||
}
|
||||
|
||||
pub fn are_transitions_ongoing(&self) -> bool {
|
||||
self.open_animation.is_some()
|
||||
|| self.resize_animation.is_some()
|
||||
|| self.move_x_animation.is_some()
|
||||
|| self.move_y_animation.is_some()
|
||||
|| self
|
||||
.alpha_animation
|
||||
.as_ref()
|
||||
.is_some_and(|alpha| !alpha.anim.is_done())
|
||||
}
|
||||
|
||||
pub fn update(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
is_active: bool,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
extra_overview_scale: f64,
|
||||
) {
|
||||
let rules = self.window.rules();
|
||||
|
||||
self.extra_overview_scale = extra_overview_scale;
|
||||
let visual_scale = self.scale * extra_overview_scale;
|
||||
|
||||
let draw_border_with_background = rules
|
||||
.draw_border_with_background
|
||||
.unwrap_or_else(|| !self.window.has_ssd());
|
||||
@@ -323,7 +385,23 @@ impl<W: LayoutElement> Tile<W> {
|
||||
view_rect.size,
|
||||
),
|
||||
radius,
|
||||
self.scale,
|
||||
visual_scale,
|
||||
1.,
|
||||
);
|
||||
|
||||
let radius = if self.is_fullscreen {
|
||||
CornerRadius::default()
|
||||
} else if self.effective_border_width().is_some() {
|
||||
radius
|
||||
} else {
|
||||
rules.geometry_corner_radius.unwrap_or_default()
|
||||
};
|
||||
self.shadow.update_render_elements(
|
||||
self.animated_tile_size(),
|
||||
is_active,
|
||||
radius,
|
||||
visual_scale,
|
||||
1.,
|
||||
);
|
||||
|
||||
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
|
||||
@@ -331,21 +409,15 @@ impl<W: LayoutElement> Tile<W> {
|
||||
} else {
|
||||
draw_border_with_background
|
||||
};
|
||||
let radius = if self.is_fullscreen {
|
||||
CornerRadius::default()
|
||||
} else if self.effective_border_width().is_some() {
|
||||
radius
|
||||
} else {
|
||||
rules.geometry_corner_radius.unwrap_or_default()
|
||||
}
|
||||
.expanded_by(self.focus_ring.width() as f32);
|
||||
let radius = radius.expanded_by(self.focus_ring.width() as f32);
|
||||
self.focus_ring.update_render_elements(
|
||||
self.animated_tile_size(),
|
||||
is_active,
|
||||
!draw_focus_ring_with_background,
|
||||
view_rect,
|
||||
radius,
|
||||
self.scale,
|
||||
visual_scale,
|
||||
1.,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -430,6 +502,39 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.move_y_animation = None;
|
||||
}
|
||||
|
||||
pub fn animate_alpha(&mut self, from: f64, to: f64, config: niri_config::Animation) {
|
||||
let from = from.clamp(0., 1.);
|
||||
let to = to.clamp(0., 1.);
|
||||
|
||||
let (current, offscreen) = if let Some(alpha) = self.alpha_animation.take() {
|
||||
(alpha.anim.clamped_value(), alpha.offscreen)
|
||||
} else {
|
||||
(from, OffscreenBuffer::default())
|
||||
};
|
||||
|
||||
self.alpha_animation = Some(AlphaAnimation {
|
||||
anim: Animation::new(self.clock.clone(), current, to, 0., config),
|
||||
hold_after_done: false,
|
||||
offscreen,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn ensure_alpha_animates_to_1(&mut self) {
|
||||
if let Some(alpha) = &self.alpha_animation {
|
||||
if alpha.anim.to() != 1. {
|
||||
// Cancel animation instead of starting a new one because the user likely wants to
|
||||
// see the tile right away.
|
||||
self.alpha_animation = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hold_alpha_animation_after_done(&mut self) {
|
||||
if let Some(alpha) = &mut self.alpha_animation {
|
||||
alpha.hold_after_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &W {
|
||||
&self.window
|
||||
}
|
||||
@@ -455,6 +560,12 @@ impl<W: LayoutElement> Tile<W> {
|
||||
Some(self.border.width())
|
||||
}
|
||||
|
||||
pub fn visual_effective_border_width(&self) -> Option<f64> {
|
||||
let visual_scale = self.scale * self.extra_overview_scale;
|
||||
self.effective_border_width()
|
||||
.map(move |w| round_logical_in_physical_max1(visual_scale, w))
|
||||
}
|
||||
|
||||
/// Returns the location of the window's visual geometry within this Tile.
|
||||
pub fn window_loc(&self) -> Point<f64, Logical> {
|
||||
let mut loc = Point::from((0., 0.));
|
||||
@@ -486,6 +597,38 @@ impl<W: LayoutElement> Tile<W> {
|
||||
loc
|
||||
}
|
||||
|
||||
pub fn visual_window_loc(&self) -> Point<f64, Logical> {
|
||||
let mut loc = Point::from((0., 0.));
|
||||
|
||||
let visual_scale = self.scale * self.extra_overview_scale;
|
||||
|
||||
// In fullscreen, center the window in the given size.
|
||||
if self.is_fullscreen {
|
||||
let window_size = self.window_size();
|
||||
let target_size = self.view_size;
|
||||
|
||||
// Windows aren't supposed to be larger than the fullscreen size, but in case we get
|
||||
// one, leave it at the top-left as usual.
|
||||
if window_size.w < target_size.w {
|
||||
loc.x += (target_size.w - window_size.w) / 2.;
|
||||
}
|
||||
if window_size.h < target_size.h {
|
||||
loc.y += (target_size.h - window_size.h) / 2.;
|
||||
}
|
||||
|
||||
// Round to physical pixels.
|
||||
loc = loc
|
||||
.to_physical_precise_round(visual_scale)
|
||||
.to_logical(visual_scale);
|
||||
}
|
||||
|
||||
if let Some(width) = self.visual_effective_border_width() {
|
||||
loc += (width, width).into();
|
||||
}
|
||||
|
||||
loc
|
||||
}
|
||||
|
||||
pub fn tile_size(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.window_size();
|
||||
|
||||
@@ -541,9 +684,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
size
|
||||
}
|
||||
|
||||
fn animated_window_size(&self) -> Size<f64, Logical> {
|
||||
pub fn animated_window_size(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.window_size();
|
||||
|
||||
let visual_scale = self.scale * self.extra_overview_scale;
|
||||
|
||||
if let Some(resize) = &self.resize_animation {
|
||||
let val = resize.anim.value();
|
||||
let size_from = resize.size_from.to_f64();
|
||||
@@ -551,25 +696,30 @@ impl<W: LayoutElement> Tile<W> {
|
||||
size.w = f64::max(1., size_from.w + (size.w - size_from.w) * val);
|
||||
size.h = f64::max(1., size_from.h + (size.h - size_from.h) * val);
|
||||
size = size
|
||||
.to_physical_precise_round(self.scale)
|
||||
.to_logical(self.scale);
|
||||
.to_physical_precise_round(visual_scale)
|
||||
.to_logical(visual_scale);
|
||||
}
|
||||
|
||||
size
|
||||
}
|
||||
|
||||
fn animated_tile_size(&self) -> Size<f64, Logical> {
|
||||
pub fn animated_tile_size(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.animated_window_size();
|
||||
|
||||
let visual_scale = self.scale * self.extra_overview_scale;
|
||||
|
||||
if self.is_fullscreen {
|
||||
// Normally we'd just return the fullscreen size here, but this makes things a bit
|
||||
// nicer if a fullscreen window is bigger than the fullscreen size for some reason.
|
||||
size.w = f64::max(size.w, self.view_size.w);
|
||||
size.h = f64::max(size.h, self.view_size.h);
|
||||
size = size
|
||||
.to_physical_precise_round(visual_scale)
|
||||
.to_logical(visual_scale);
|
||||
return size;
|
||||
}
|
||||
|
||||
if let Some(width) = self.effective_border_width() {
|
||||
if let Some(width) = self.visual_effective_border_width() {
|
||||
size.w += width * 2.;
|
||||
size.h += width * 2.;
|
||||
}
|
||||
@@ -584,16 +734,32 @@ impl<W: LayoutElement> Tile<W> {
|
||||
loc
|
||||
}
|
||||
|
||||
pub fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
|
||||
fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool {
|
||||
point -= self.window_loc().to_f64();
|
||||
self.window.is_in_input_region(point)
|
||||
}
|
||||
|
||||
pub fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
|
||||
fn is_in_activation_region(&self, point: Point<f64, Logical>) -> bool {
|
||||
let activation_region = Rectangle::from_size(self.tile_size());
|
||||
activation_region.contains(point)
|
||||
}
|
||||
|
||||
pub fn hit(&self, point: Point<f64, Logical>) -> Option<HitType> {
|
||||
let offset = self.bob_offset();
|
||||
let point = point - offset;
|
||||
|
||||
if self.is_in_input_region(point) {
|
||||
let win_pos = self.buf_loc() + offset;
|
||||
Some(HitType::Input { win_pos })
|
||||
} else if self.is_in_activation_region(point) {
|
||||
Some(HitType::Activate {
|
||||
is_tab_indicator: false,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_tile_size(
|
||||
&mut self,
|
||||
mut size: Size<f64, Logical>,
|
||||
@@ -611,7 +777,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// round to avoid situations where proportionally-sized columns don't fit on the screen
|
||||
// exactly.
|
||||
self.window
|
||||
.request_size(size.to_i32_floor(), animate, transaction);
|
||||
.request_size(size.to_i32_floor(), false, animate, transaction);
|
||||
}
|
||||
|
||||
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
|
||||
@@ -646,15 +812,18 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_fullscreen(&mut self) {
|
||||
pub fn request_fullscreen(&mut self, animate: bool, transaction: Option<Transaction>) {
|
||||
self.window
|
||||
.request_fullscreen(self.view_size.to_i32_round());
|
||||
.request_size(self.view_size.to_i32_round(), true, animate, transaction);
|
||||
}
|
||||
|
||||
pub fn min_size(&self) -> Size<f64, Logical> {
|
||||
pub fn min_size_nonfullscreen(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.window.min_size().to_f64();
|
||||
|
||||
if let Some(width) = self.effective_border_width() {
|
||||
// Can't go through effective_border_width() because we might be fullscreen.
|
||||
if !self.border.is_off() {
|
||||
let width = self.border.width();
|
||||
|
||||
size.w = f64::max(1., size.w);
|
||||
size.h = f64::max(1., size.h);
|
||||
|
||||
@@ -665,10 +834,13 @@ impl<W: LayoutElement> Tile<W> {
|
||||
size
|
||||
}
|
||||
|
||||
pub fn max_size(&self) -> Size<f64, Logical> {
|
||||
pub fn max_size_nonfullscreen(&self) -> Size<f64, Logical> {
|
||||
let mut size = self.window.max_size().to_f64();
|
||||
|
||||
if let Some(width) = self.effective_border_width() {
|
||||
// Can't go through effective_border_width() because we might be fullscreen.
|
||||
if !self.border.is_off() {
|
||||
let width = self.border.width();
|
||||
|
||||
if size.w > 0. {
|
||||
size.w += width * 2.;
|
||||
}
|
||||
@@ -680,6 +852,20 @@ impl<W: LayoutElement> Tile<W> {
|
||||
size
|
||||
}
|
||||
|
||||
pub fn bob_offset(&self) -> Point<f64, Logical> {
|
||||
if self.window.rules().baba_is_float != Some(true) {
|
||||
return Point::from((0., 0.));
|
||||
}
|
||||
|
||||
let visual_scale = self.scale * self.extra_overview_scale;
|
||||
|
||||
let now = self.clock.now().as_secs_f64();
|
||||
let amplitude = self.view_size.h / 96.;
|
||||
let y = amplitude * ((f64::consts::TAU * now / 3.6).sin() - 1.);
|
||||
let y = round_logical_in_physical(visual_scale, y);
|
||||
Point::from((0., y))
|
||||
}
|
||||
|
||||
pub fn draw_border_with_background(&self) -> bool {
|
||||
if self.effective_border_width().is_some() {
|
||||
return false;
|
||||
@@ -691,23 +877,35 @@ impl<W: LayoutElement> Tile<W> {
|
||||
.unwrap_or_else(|| !self.window.has_ssd())
|
||||
}
|
||||
|
||||
fn render_inner<R: NiriRenderer>(
|
||||
&self,
|
||||
fn render_inner<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
let _span = tracy_client::span!("Tile::render_inner");
|
||||
|
||||
let alpha = if self.is_fullscreen {
|
||||
let scale = Scale::from(self.scale);
|
||||
let visual_scale = scale * self.extra_overview_scale;
|
||||
|
||||
let win_alpha = if self.is_fullscreen || self.window.is_ignoring_opacity_window_rule() {
|
||||
1.
|
||||
} else {
|
||||
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
|
||||
};
|
||||
|
||||
let window_loc = self.window_loc();
|
||||
// This is here rather than in render_offset() because render_offset() is currently assumed
|
||||
// by the code to be temporary. So, for example, interactive move will try to "grab" the
|
||||
// tile at its current render offset and reset the render offset to zero by cancelling the
|
||||
// tile move animations. On the other hand, bob_offset() is not resettable, so adding it in
|
||||
// render_offset() would cause obvious animation glitches.
|
||||
//
|
||||
// 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 window_loc = self.visual_window_loc();
|
||||
let window_size = self.window_size().to_f64();
|
||||
let animated_window_size = self.animated_window_size();
|
||||
let window_render_loc = location + window_loc;
|
||||
@@ -725,7 +923,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
if let Some(resize) = &self.resize_animation {
|
||||
resize_popups = Some(
|
||||
self.window
|
||||
.render_popups(renderer, window_render_loc, scale, alpha, target)
|
||||
.render_popups(renderer, window_render_loc, scale, win_alpha, target)
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
);
|
||||
@@ -742,15 +940,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
target,
|
||||
);
|
||||
|
||||
let current = render_to_encompassing_texture(
|
||||
gles_renderer,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
&window_elements,
|
||||
)
|
||||
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
|
||||
.ok();
|
||||
let current = resize
|
||||
.offscreen
|
||||
.render(gles_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.
|
||||
@@ -763,7 +957,13 @@ impl<W: LayoutElement> Tile<W> {
|
||||
clip_to_geometry
|
||||
};
|
||||
|
||||
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
|
||||
if let Some((elem_current, _sync_point, mut data)) = current {
|
||||
let texture_current = elem_current.texture().clone();
|
||||
// The offset and size are computed in physical pixels and converted to
|
||||
// logical with the same `scale`, so converting them back with rounding
|
||||
// inside the geometry() call gives us the same physical result back.
|
||||
let texture_current_geo = elem_current.geometry(scale);
|
||||
|
||||
let elem = ResizeRenderElement::new(
|
||||
area,
|
||||
scale,
|
||||
@@ -775,12 +975,15 @@ impl<W: LayoutElement> Tile<W> {
|
||||
resize.anim.clamped_value().clamp(0., 1.) as f32,
|
||||
radius,
|
||||
clip_to_geometry,
|
||||
alpha,
|
||||
win_alpha,
|
||||
);
|
||||
// FIXME: with split popups, this will use the resize element ID for
|
||||
// popups, but we want the real IDs.
|
||||
self.window
|
||||
.set_offscreen_element_id(Some(elem.id().clone()));
|
||||
|
||||
// We're drawing the resize shader, not the offscreen directly.
|
||||
data.id = elem.id().clone();
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -792,12 +995,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
area.loc,
|
||||
alpha,
|
||||
win_alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
self.window.set_offscreen_element_id(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,7 +1010,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
if resize_shader.is_none() && resize_fallback.is_none() {
|
||||
let window = self
|
||||
.window
|
||||
.render(renderer, window_render_loc, scale, alpha, target);
|
||||
.render(renderer, window_render_loc, scale, win_alpha, target);
|
||||
|
||||
let geo = Rectangle::new(window_render_loc, window_size);
|
||||
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
|
||||
@@ -861,6 +1063,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
0.,
|
||||
radius,
|
||||
scale.x as f32,
|
||||
1.,
|
||||
)
|
||||
.with_location(geo.loc)
|
||||
.into();
|
||||
@@ -894,7 +1097,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
});
|
||||
let rv = rv.chain(elem);
|
||||
|
||||
let elem = self.effective_border_width().map(|width| {
|
||||
let elem = self.visual_effective_border_width().map(|width| {
|
||||
self.border
|
||||
.render(renderer, location + Point::from((width, width)))
|
||||
.map(Into::into)
|
||||
@@ -902,81 +1105,98 @@ impl<W: LayoutElement> Tile<W> {
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
|
||||
let elem = focus_ring.then(|| self.focus_ring.render(renderer, location).map(Into::into));
|
||||
rv.chain(elem.into_iter().flatten())
|
||||
let rv = rv.chain(elem.into_iter().flatten());
|
||||
|
||||
rv.chain(self.shadow.render(renderer, location).map(Into::into))
|
||||
}
|
||||
|
||||
pub fn render<R: NiriRenderer>(
|
||||
&self,
|
||||
pub fn render<'a, R: NiriRenderer + 'a>(
|
||||
&'a self,
|
||||
renderer: &mut R,
|
||||
location: Point<f64, Logical>,
|
||||
scale: Scale<f64>,
|
||||
focus_ring: bool,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
let _span = tracy_client::span!("Tile::render");
|
||||
|
||||
let scale = Scale::from(self.scale);
|
||||
|
||||
let tile_alpha = self
|
||||
.alpha_animation
|
||||
.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;
|
||||
|
||||
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.)), scale, focus_ring, target);
|
||||
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
|
||||
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||
match open.render(renderer, &elements, self.tile_size(), location, scale) {
|
||||
Ok(elem) => {
|
||||
self.window()
|
||||
.set_offscreen_element_id(Some(elem.id().clone()));
|
||||
match open.render(
|
||||
renderer,
|
||||
&elements,
|
||||
self.animated_tile_size(),
|
||||
location,
|
||||
scale,
|
||||
tile_alpha,
|
||||
) {
|
||||
Ok((elem, data)) => {
|
||||
self.window().set_offscreen_data(Some(data));
|
||||
open_anim_elem = Some(elem.into());
|
||||
}
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error rendering tile to offscreen for alpha animation: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if open_anim_elem.is_none() {
|
||||
self.window().set_offscreen_element_id(None);
|
||||
window_elems = Some(self.render_inner(renderer, location, scale, focus_ring, target));
|
||||
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
|
||||
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
|
||||
}
|
||||
|
||||
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,
|
||||
scale: Scale<f64>,
|
||||
) {
|
||||
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
|
||||
if self.unmap_snapshot.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.unmap_snapshot = Some(self.render_snapshot(renderer, scale));
|
||||
self.unmap_snapshot = Some(self.render_snapshot(renderer));
|
||||
}
|
||||
|
||||
fn render_snapshot(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: Scale<f64>,
|
||||
) -> TileRenderSnapshot {
|
||||
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
|
||||
let _span = tracy_client::span!("Tile::render_snapshot");
|
||||
|
||||
let contents = self.render(
|
||||
renderer,
|
||||
Point::from((0., 0.)),
|
||||
scale,
|
||||
false,
|
||||
RenderTarget::Output,
|
||||
);
|
||||
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,
|
||||
Point::from((0., 0.)),
|
||||
scale,
|
||||
false,
|
||||
RenderTarget::Screencast,
|
||||
);
|
||||
@@ -995,6 +1215,14 @@ impl<W: LayoutElement> Tile<W> {
|
||||
self.unmap_snapshot.take()
|
||||
}
|
||||
|
||||
pub fn border(&self) -> &FocusRing {
|
||||
&self.border
|
||||
}
|
||||
|
||||
pub fn focus_ring(&self) -> &FocusRing {
|
||||
&self.focus_ring
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &Rc<Options> {
|
||||
&self.options
|
||||
}
|
||||
|
||||
+239
-56
@@ -2,14 +2,17 @@ use std::cmp::max;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig};
|
||||
use niri_ipc::{PositionChange, SizeChange};
|
||||
use niri_config::{
|
||||
CenterFocusedColumn, CornerRadius, FloatOrInt, OutputName, PresetSize,
|
||||
Workspace as WorkspaceConfig,
|
||||
};
|
||||
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::{layer_map_for_output, Window};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Serial, Size, Transform};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::SurfaceCachedState;
|
||||
|
||||
@@ -18,11 +21,15 @@ use super::scrolling::{
|
||||
Column, ColumnWidth, InsertHint, InsertPosition, ScrollDirection, ScrollingSpace,
|
||||
ScrollingSpaceRenderElement,
|
||||
};
|
||||
use super::shadow::Shadow;
|
||||
use super::tile::{Tile, TileRenderSnapshot};
|
||||
use super::{ActivateWindow, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac};
|
||||
use super::{
|
||||
ActivateWindow, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile, SizeFrac,
|
||||
};
|
||||
use crate::animation::Clock;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
@@ -78,6 +85,9 @@ pub struct Workspace<W: LayoutElement> {
|
||||
/// zones.
|
||||
working_area: Rectangle<f64, Logical>,
|
||||
|
||||
/// This workspace's shadow in the overview.
|
||||
shadow: Shadow,
|
||||
|
||||
/// Clock for driving animations.
|
||||
pub(super) clock: Clock,
|
||||
|
||||
@@ -226,6 +236,17 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config = niri_config::Shadow {
|
||||
on: true,
|
||||
offset: niri_config::ShadowOffset {
|
||||
x: FloatOrInt(0.),
|
||||
y: FloatOrInt(20.),
|
||||
},
|
||||
softness: FloatOrInt(120.),
|
||||
spread: FloatOrInt(20.),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
scrolling,
|
||||
floating,
|
||||
@@ -235,6 +256,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
transform: output.current_transform(),
|
||||
view_size,
|
||||
working_area,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
output: Some(output),
|
||||
clock,
|
||||
base_options,
|
||||
@@ -279,6 +301,17 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config = niri_config::Shadow {
|
||||
on: true,
|
||||
offset: niri_config::ShadowOffset {
|
||||
x: FloatOrInt(0.),
|
||||
y: FloatOrInt(20.),
|
||||
},
|
||||
softness: FloatOrInt(120.),
|
||||
spread: FloatOrInt(20.),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
scrolling,
|
||||
floating,
|
||||
@@ -289,6 +322,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
original_output,
|
||||
view_size,
|
||||
working_area,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
clock,
|
||||
base_options,
|
||||
options,
|
||||
@@ -331,16 +365,37 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
|
||||
pub fn are_transitions_ongoing(&self) -> bool {
|
||||
self.scrolling.are_transitions_ongoing() || self.floating.are_animations_ongoing()
|
||||
self.scrolling.are_transitions_ongoing() || self.floating.are_transitions_ongoing()
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, is_active: bool) {
|
||||
self.scrolling
|
||||
.update_render_elements(is_active && !self.floating_is_active.get());
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
is_active: bool,
|
||||
is_overview_open: bool,
|
||||
extra_overview_scale: f64,
|
||||
) {
|
||||
self.scrolling.update_render_elements(
|
||||
is_active && !self.floating_is_active.get(),
|
||||
is_overview_open,
|
||||
extra_overview_scale,
|
||||
);
|
||||
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
self.floating
|
||||
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
|
||||
self.floating.update_render_elements(
|
||||
is_active && self.floating_is_active.get(),
|
||||
view_rect,
|
||||
extra_overview_scale,
|
||||
);
|
||||
|
||||
let visual_scale = self.scale.fractional_scale() * extra_overview_scale;
|
||||
|
||||
self.shadow.update_render_elements(
|
||||
self.view_size,
|
||||
true,
|
||||
CornerRadius::default(),
|
||||
visual_scale,
|
||||
1.,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, base_options: Rc<Options>) {
|
||||
@@ -368,6 +423,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.scrolling.update_shaders();
|
||||
self.floating.update_shaders();
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
|
||||
@@ -406,6 +462,14 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_window_mut(&mut self) -> Option<&mut W> {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.active_window_mut()
|
||||
} else {
|
||||
self.scrolling.active_window_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active_fullscreen(&self) -> bool {
|
||||
self.scrolling.is_active_fullscreen()
|
||||
}
|
||||
@@ -567,10 +631,10 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.floating.add_tile_above(next_to, tile, activate);
|
||||
} else {
|
||||
// FIXME: use static pos
|
||||
let (next_to_tile, render_pos) = self
|
||||
let (next_to_tile, render_pos, _visible) = self
|
||||
.scrolling
|
||||
.tiles_with_render_positions()
|
||||
.find(|(tile, _)| tile.window().id() == next_to)
|
||||
.find(|(tile, _, _)| tile.window().id() == next_to)
|
||||
.unwrap();
|
||||
|
||||
// Position the new tile in the center above the next_to tile. Think a
|
||||
@@ -705,9 +769,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
pub fn resolve_default_width(
|
||||
&self,
|
||||
default_width: Option<Option<ColumnWidth>>,
|
||||
default_width: Option<Option<PresetSize>>,
|
||||
is_floating: bool,
|
||||
) -> Option<ColumnWidth> {
|
||||
) -> Option<PresetSize> {
|
||||
match default_width {
|
||||
Some(Some(width)) => Some(width),
|
||||
Some(None) => None,
|
||||
@@ -732,7 +796,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
pub fn new_window_size(
|
||||
&self,
|
||||
width: Option<ColumnWidth>,
|
||||
width: Option<PresetSize>,
|
||||
height: Option<PresetSize>,
|
||||
is_floating: bool,
|
||||
rules: &ResolvedWindowRules,
|
||||
@@ -764,7 +828,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
pub fn configure_new_window(
|
||||
&self,
|
||||
window: &Window,
|
||||
width: Option<ColumnWidth>,
|
||||
width: Option<PresetSize>,
|
||||
height: Option<PresetSize>,
|
||||
is_floating: bool,
|
||||
rules: &ResolvedWindowRules,
|
||||
@@ -789,9 +853,9 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
|
||||
if is_floating {
|
||||
state.bounds = Some(self.floating.toplevel_bounds(rules));
|
||||
state.bounds = Some(self.floating.new_window_toplevel_bounds(rules));
|
||||
} else {
|
||||
state.bounds = Some(self.scrolling.toplevel_bounds(rules));
|
||||
state.bounds = Some(self.scrolling.new_window_toplevel_bounds(rules));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -840,6 +904,20 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_column(&mut self, index: usize) {
|
||||
if self.floating_is_active.get() {
|
||||
self.focus_tiling();
|
||||
}
|
||||
self.scrolling.focus_column(index);
|
||||
}
|
||||
|
||||
pub fn focus_window_in_column(&mut self, index: u8) {
|
||||
if self.floating_is_active.get() {
|
||||
return;
|
||||
}
|
||||
self.scrolling.focus_window_in_column(index);
|
||||
}
|
||||
|
||||
pub fn focus_down(&mut self) -> bool {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.focus_down()
|
||||
@@ -888,6 +966,34 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_window_top(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.focus_topmost();
|
||||
} else {
|
||||
self.scrolling.focus_top();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_window_bottom(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.focus_bottommost();
|
||||
} else {
|
||||
self.scrolling.focus_bottom();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_window_down_or_top(&mut self) {
|
||||
if !self.focus_down() {
|
||||
self.focus_window_top();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_window_up_or_bottom(&mut self) {
|
||||
if !self.focus_up() {
|
||||
self.focus_window_bottom();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) -> bool {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.move_left();
|
||||
@@ -920,6 +1026,13 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.scrolling.move_column_to_last();
|
||||
}
|
||||
|
||||
pub fn move_column_to_index(&mut self, index: usize) {
|
||||
if self.floating_is_active.get() {
|
||||
return;
|
||||
}
|
||||
self.scrolling.move_column_to_index(index);
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) -> bool {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.move_down();
|
||||
@@ -977,6 +1090,20 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.scrolling.swap_window_in_direction(direction);
|
||||
}
|
||||
|
||||
pub fn toggle_column_tabbed_display(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
return;
|
||||
}
|
||||
self.scrolling.toggle_column_tabbed_display();
|
||||
}
|
||||
|
||||
pub fn set_column_display(&mut self, display: ColumnDisplay) {
|
||||
if self.floating_is_active.get() {
|
||||
return;
|
||||
}
|
||||
self.scrolling.set_column_display(display);
|
||||
}
|
||||
|
||||
pub fn center_column(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
self.floating.center_window(None);
|
||||
@@ -1069,6 +1196,13 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_column_to_available_width(&mut self) {
|
||||
if self.floating_is_active.get() {
|
||||
return;
|
||||
}
|
||||
self.scrolling.expand_column_to_available_width();
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
|
||||
let mut unfullscreen_to_floating = false;
|
||||
if self.floating.has_window(window) {
|
||||
@@ -1291,7 +1425,6 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> {
|
||||
let scrolling = self.scrolling.tiles_with_render_positions();
|
||||
let scrolling = scrolling.map(|(tile, pos)| (tile, pos, true));
|
||||
|
||||
let floating = self.floating.tiles_with_render_positions();
|
||||
let visible = self.is_floating_visible();
|
||||
@@ -1330,30 +1463,36 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
renderer: &mut R,
|
||||
target: RenderTarget,
|
||||
focus_ring: bool,
|
||||
is_overview_open: bool,
|
||||
) -> impl Iterator<Item = WorkspaceRenderElement<R>> {
|
||||
let scale = Scale::from(self.scale.fractional_scale());
|
||||
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
|
||||
let scrolling =
|
||||
self.scrolling
|
||||
.render_elements(renderer, scale, target, scrolling_focus_ring);
|
||||
let scrolling = self.scrolling.render_elements(
|
||||
renderer,
|
||||
target,
|
||||
scrolling_focus_ring,
|
||||
is_overview_open,
|
||||
);
|
||||
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
|
||||
|
||||
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,
|
||||
scale,
|
||||
target,
|
||||
floating_focus_ring,
|
||||
);
|
||||
let floating =
|
||||
self.floating
|
||||
.render_elements(renderer, view_rect, target, floating_focus_ring);
|
||||
floating.into_iter().map(WorkspaceRenderElement::from)
|
||||
});
|
||||
|
||||
floating.into_iter().flatten().chain(scrolling)
|
||||
}
|
||||
|
||||
pub fn render_shadow<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)))
|
||||
}
|
||||
|
||||
pub fn render_above_top_layer(&self) -> bool {
|
||||
self.scrolling.render_above_top_layer()
|
||||
}
|
||||
@@ -1367,14 +1506,13 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
}
|
||||
|
||||
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
|
||||
let output_scale = Scale::from(self.scale.fractional_scale());
|
||||
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(false, view_rect);
|
||||
tile.store_unmap_snapshot_if_empty(renderer, output_scale);
|
||||
tile.update_render_elements(false, view_rect, 1.);
|
||||
tile.store_unmap_snapshot_if_empty(renderer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1416,27 +1554,23 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
|
||||
}
|
||||
|
||||
pub fn window_under(
|
||||
&self,
|
||||
pos: Point<f64, Logical>,
|
||||
) -> Option<(&W, Option<Point<f64, Logical>>)> {
|
||||
self.tiles_with_render_positions()
|
||||
.find_map(|(tile, tile_pos, visible)| {
|
||||
if !visible {
|
||||
return None;
|
||||
}
|
||||
pub fn start_open_animation(&mut self, id: &W::Id) -> bool {
|
||||
self.scrolling.start_open_animation(id) || self.floating.start_open_animation(id)
|
||||
}
|
||||
|
||||
let pos_within_tile = pos - tile_pos;
|
||||
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, HitType)> {
|
||||
// This logic is consistent with tiles_with_render_positions().
|
||||
if self.is_floating_visible() {
|
||||
if let Some(rv) = self
|
||||
.floating
|
||||
.tiles_with_render_positions()
|
||||
.find_map(|(tile, tile_pos)| HitType::hit_tile(tile, tile_pos, pos))
|
||||
{
|
||||
return Some(rv);
|
||||
}
|
||||
}
|
||||
|
||||
if tile.is_in_input_region(pos_within_tile) {
|
||||
let pos_within_surface = tile_pos + tile.buf_loc();
|
||||
return Some((tile.window(), Some(pos_within_surface)));
|
||||
} else if tile.is_in_activation_region(pos_within_tile) {
|
||||
return Some((tile.window(), None));
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
self.scrolling.window_under(pos)
|
||||
}
|
||||
|
||||
pub fn resize_edges_under(&self, pos: Point<f64, Logical>) -> Option<ResizeEdge> {
|
||||
@@ -1450,9 +1584,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
|
||||
let pos_within_tile = pos - tile_pos;
|
||||
|
||||
if tile.is_in_input_region(pos_within_tile)
|
||||
|| tile.is_in_activation_region(pos_within_tile)
|
||||
{
|
||||
if tile.hit(pos_within_tile).is_some() {
|
||||
let size = tile.tile_size().to_f64();
|
||||
|
||||
let mut edges = ResizeEdge::empty();
|
||||
@@ -1557,6 +1689,45 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
.view_offset_gesture_end(cancelled, is_touchpad)
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_begin(&mut self) {
|
||||
self.scrolling.dnd_scroll_gesture_begin();
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) -> bool {
|
||||
let config = &self.options.gestures.dnd_edge_view_scroll;
|
||||
let trigger_width = config.trigger_width.0;
|
||||
|
||||
// This working area intentionally does not include extra struts from Options.
|
||||
let x = pos.x - self.working_area.loc.x;
|
||||
let width = self.working_area.size.w;
|
||||
|
||||
let x = x.clamp(0., width);
|
||||
let trigger_width = trigger_width.clamp(0., width / 2.);
|
||||
|
||||
let delta = if x < trigger_width {
|
||||
-(trigger_width - x)
|
||||
} else if width - x < trigger_width {
|
||||
trigger_width - (width - x)
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let delta = if trigger_width < 0.01 {
|
||||
// Sanity check for trigger-width 0 or small window sizes.
|
||||
0.
|
||||
} else {
|
||||
// Normalize to [0, 1].
|
||||
delta / trigger_width
|
||||
};
|
||||
let delta = delta * speed;
|
||||
|
||||
self.scrolling.dnd_scroll_gesture_scroll(delta)
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_end(&mut self) {
|
||||
self.scrolling.dnd_scroll_gesture_end();
|
||||
}
|
||||
|
||||
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
|
||||
if self.floating.has_window(&window) {
|
||||
self.floating.interactive_resize_begin(window, edges)
|
||||
@@ -1644,7 +1815,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
);
|
||||
}
|
||||
|
||||
for (tile, tile_pos, _visible) in self.tiles_with_render_positions() {
|
||||
for (tile, tile_pos, visible) in self.tiles_with_render_positions() {
|
||||
if Some(tile.window().id()) != move_win_id {
|
||||
assert_eq!(tile.interactive_move_offset, Point::from((0., 0.)));
|
||||
}
|
||||
@@ -1654,6 +1825,18 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
// Tile positions must be rounded to physical pixels.
|
||||
assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5);
|
||||
assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5);
|
||||
|
||||
if let Some(alpha) = &tile.alpha_animation {
|
||||
let anim = &alpha.anim;
|
||||
if visible {
|
||||
assert_eq!(anim.to(), 1., "visible tiles can animate alpha only to 1");
|
||||
}
|
||||
|
||||
assert!(
|
||||
!alpha.hold_after_done,
|
||||
"tiles in the layout cannot have held alpha animation"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-20
@@ -5,11 +5,11 @@ use std::fmt::Write as _;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::{env, mem};
|
||||
|
||||
use clap::Parser;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use directories::ProjectDirs;
|
||||
use niri::cli::{Cli, Sub};
|
||||
#[cfg(feature = "dbus")]
|
||||
@@ -48,6 +48,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
|
||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
.with_writer(io::stderr)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
|
||||
if env::var_os("NOTIFY_SOCKET").is_some() {
|
||||
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
|
||||
|
||||
@@ -58,19 +66,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
}
|
||||
|
||||
let directives = env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_FILTER.to_owned());
|
||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.session {
|
||||
// If we're starting as a session, assume that the intention is to start on a TTY. Remove
|
||||
// DISPLAY or WAYLAND_DISPLAY from our environment if they are set, since they will cause
|
||||
// the winit backend to be selected instead.
|
||||
// DISPLAY, WAYLAND_DISPLAY or WAYLAND_SOCKET from our environment if they are set, since
|
||||
// they will cause the winit backend to be selected instead.
|
||||
if env::var_os("DISPLAY").is_some() {
|
||||
warn!("running as a session but DISPLAY is set, removing it");
|
||||
env::remove_var("DISPLAY");
|
||||
@@ -79,6 +80,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
|
||||
env::remove_var("WAYLAND_DISPLAY");
|
||||
}
|
||||
if env::var_os("WAYLAND_SOCKET").is_some() {
|
||||
warn!("running as a session but WAYLAND_SOCKET is set, removing it");
|
||||
env::remove_var("WAYLAND_SOCKET");
|
||||
}
|
||||
|
||||
// Set the current desktop for xdg-desktop-portal.
|
||||
env::set_var("XDG_CURRENT_DESKTOP", "niri");
|
||||
@@ -86,9 +91,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||
}
|
||||
|
||||
// Set a better error printer for config loading.
|
||||
niri_config::set_miette_hook().unwrap();
|
||||
|
||||
// Handle subcommands.
|
||||
if let Some(subcommand) = cli.subcommand {
|
||||
match subcommand {
|
||||
@@ -105,6 +107,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
Sub::Panic => cause_panic(),
|
||||
Sub::Completions { shell } => {
|
||||
clap_complete::generate(shell, &mut Cli::command(), "niri", &mut io::stdout());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,11 +183,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Set WAYLAND_DISPLAY for children.
|
||||
let socket_name = &state.niri.socket_name;
|
||||
let socket_name = state.niri.socket_name.as_deref().unwrap();
|
||||
env::set_var("WAYLAND_DISPLAY", socket_name);
|
||||
info!(
|
||||
"listening on Wayland socket: {}",
|
||||
@@ -190,8 +197,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Set NIRI_SOCKET for children.
|
||||
if let Some(ipc) = &state.niri.ipc_server {
|
||||
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
|
||||
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
||||
let socket_path = ipc.socket_path.as_deref().unwrap();
|
||||
env::set_var(SOCKET_PATH_ENV, socket_path);
|
||||
info!("IPC listening on: {}", socket_path.to_string_lossy());
|
||||
}
|
||||
|
||||
if cli.session {
|
||||
@@ -224,12 +232,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Set up config file watcher.
|
||||
let _watcher = {
|
||||
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
|
||||
// watcher thread.
|
||||
let process = |path: &Path| {
|
||||
Config::load(path).map_err(|err| {
|
||||
warn!("{:?}", err.context("error loading config"));
|
||||
})
|
||||
};
|
||||
|
||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||
let watcher = Watcher::new(watch_path.clone(), tx);
|
||||
let watcher = Watcher::new(watch_path.clone(), process, tx);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(rx, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
|
||||
.insert_source(rx, |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(config) => state.reload_config(config),
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
+1130
-434
File diff suppressed because it is too large
Load Diff
@@ -3,5 +3,6 @@ pub mod gamma_control;
|
||||
pub mod mutter_x11_interop;
|
||||
pub mod output_management;
|
||||
pub mod screencopy;
|
||||
pub mod virtual_pointer;
|
||||
|
||||
pub mod raw;
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device,
|
||||
DeviceCapability, Event, InputBackend, PointerAxisEvent, PointerButtonEvent,
|
||||
PointerMotionAbsoluteEvent, PointerMotionEvent, UnusedEvent,
|
||||
};
|
||||
use smithay::input::pointer::AxisFrame;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols_wlr;
|
||||
use smithay::reexports::wayland_server::protocol::wl_pointer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use wayland_backend::protocol::WEnum;
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::server::{
|
||||
zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1,
|
||||
};
|
||||
use zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1;
|
||||
use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1;
|
||||
|
||||
const VERSION: u32 = 2;
|
||||
|
||||
pub struct VirtualPointerManagerState {
|
||||
virtual_pointers: HashSet<ZwlrVirtualPointerV1>,
|
||||
}
|
||||
|
||||
pub struct VirtualPointerManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
pub struct VirtualPointerInputBackend;
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct VirtualPointer {
|
||||
pointer: ZwlrVirtualPointerV1,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VirtualPointerUserData {
|
||||
seat: Option<WlSeat>,
|
||||
output: Option<Output>,
|
||||
|
||||
axis_frame: Mutex<Option<AxisFrame>>,
|
||||
}
|
||||
|
||||
impl VirtualPointer {
|
||||
fn data(&self) -> &VirtualPointerUserData {
|
||||
self.pointer.data().unwrap()
|
||||
}
|
||||
|
||||
pub fn seat(&self) -> Option<&WlSeat> {
|
||||
self.data().seat.as_ref()
|
||||
}
|
||||
|
||||
pub fn output(&self) -> Option<&Output> {
|
||||
self.data().output.as_ref()
|
||||
}
|
||||
|
||||
fn finish_axis_frame(&self) -> Option<AxisFrame> {
|
||||
self.data().axis_frame.lock().unwrap().take()
|
||||
}
|
||||
|
||||
fn mutate_axis_frame(&self, time: Option<u32>, f: impl FnOnce(AxisFrame) -> AxisFrame) {
|
||||
let mut frame = self.data().axis_frame.lock().unwrap();
|
||||
|
||||
*frame = frame.or(time.map(AxisFrame::new)).map(f);
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for VirtualPointer {
|
||||
fn id(&self) -> String {
|
||||
format!("wlr virtual pointer {}", self.pointer.id())
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
String::from("virtual pointer")
|
||||
}
|
||||
|
||||
fn has_capability(&self, capability: DeviceCapability) -> bool {
|
||||
matches!(capability, DeviceCapability::Pointer)
|
||||
}
|
||||
|
||||
fn usb_id(&self) -> Option<(u32, u32)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn syspath(&self) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualPointerMotionEvent {
|
||||
pointer: VirtualPointer,
|
||||
time: u32,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
}
|
||||
|
||||
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
|
||||
fn time(&self) -> u64 {
|
||||
self.time as u64 * 1000 // millis to micros
|
||||
}
|
||||
|
||||
fn device(&self) -> VirtualPointer {
|
||||
self.pointer.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerMotionEvent<VirtualPointerInputBackend> for VirtualPointerMotionEvent {
|
||||
fn delta_x(&self) -> f64 {
|
||||
self.dx
|
||||
}
|
||||
|
||||
fn delta_y(&self) -> f64 {
|
||||
self.dy
|
||||
}
|
||||
|
||||
fn delta_x_unaccel(&self) -> f64 {
|
||||
self.dx
|
||||
}
|
||||
|
||||
fn delta_y_unaccel(&self) -> f64 {
|
||||
self.dy
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualPointerMotionAbsoluteEvent {
|
||||
pointer: VirtualPointer,
|
||||
time: u32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
x_extent: u32,
|
||||
y_extent: u32,
|
||||
}
|
||||
|
||||
impl Event<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
|
||||
fn time(&self) -> u64 {
|
||||
self.time as u64 * 1000 // millis to micros
|
||||
}
|
||||
|
||||
fn device(&self) -> VirtualPointer {
|
||||
self.pointer.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl AbsolutePositionEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {
|
||||
fn x(&self) -> f64 {
|
||||
self.x as f64 / self.x_extent as f64
|
||||
}
|
||||
|
||||
fn y(&self) -> f64 {
|
||||
self.y as f64 / self.y_extent as f64
|
||||
}
|
||||
|
||||
fn x_transformed(&self, width: i32) -> f64 {
|
||||
(self.x as i64 * width as i64) as f64 / self.x_extent as f64
|
||||
}
|
||||
|
||||
fn y_transformed(&self, height: i32) -> f64 {
|
||||
(self.y as i64 * height as i64) as f64 / self.y_extent as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualPointerButtonEvent {
|
||||
pointer: VirtualPointer,
|
||||
time: u32,
|
||||
button: u32,
|
||||
state: ButtonState,
|
||||
}
|
||||
|
||||
impl Event<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
|
||||
fn time(&self) -> u64 {
|
||||
self.time as u64 * 1000 // millis to micros
|
||||
}
|
||||
|
||||
fn device(&self) -> VirtualPointer {
|
||||
self.pointer.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerButtonEvent<VirtualPointerInputBackend> for VirtualPointerButtonEvent {
|
||||
fn button_code(&self) -> u32 {
|
||||
self.button
|
||||
}
|
||||
|
||||
fn state(&self) -> ButtonState {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VirtualPointerAxisEvent {
|
||||
pointer: VirtualPointer,
|
||||
frame: AxisFrame,
|
||||
}
|
||||
|
||||
impl Event<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
|
||||
fn time(&self) -> u64 {
|
||||
self.frame.time as u64 * 1000 // millis to micros
|
||||
}
|
||||
|
||||
fn device(&self) -> VirtualPointer {
|
||||
self.pointer.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn tuple_axis<T>(tuple: (T, T), axis: Axis) -> T {
|
||||
match axis {
|
||||
Axis::Horizontal => tuple.0,
|
||||
Axis::Vertical => tuple.1,
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerAxisEvent<VirtualPointerInputBackend> for VirtualPointerAxisEvent {
|
||||
fn amount(&self, axis: Axis) -> Option<f64> {
|
||||
Some(tuple_axis(self.frame.axis, axis))
|
||||
}
|
||||
|
||||
fn amount_v120(&self, axis: Axis) -> Option<f64> {
|
||||
self.frame.v120.map(|v120| tuple_axis(v120, axis) as f64)
|
||||
}
|
||||
|
||||
fn source(&self) -> AxisSource {
|
||||
self.frame.source.unwrap_or_else(|| {
|
||||
warn!("AxisSource: no source set, giving bogus value");
|
||||
AxisSource::Continuous
|
||||
})
|
||||
}
|
||||
|
||||
fn relative_direction(&self, axis: Axis) -> AxisRelativeDirection {
|
||||
tuple_axis(self.frame.relative_direction, axis)
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerMotionAbsoluteEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {}
|
||||
|
||||
impl InputBackend for VirtualPointerInputBackend {
|
||||
type Device = VirtualPointer;
|
||||
|
||||
type KeyboardKeyEvent = UnusedEvent;
|
||||
type PointerAxisEvent = VirtualPointerAxisEvent;
|
||||
type PointerButtonEvent = VirtualPointerButtonEvent;
|
||||
type PointerMotionEvent = VirtualPointerMotionEvent;
|
||||
type PointerMotionAbsoluteEvent = VirtualPointerMotionAbsoluteEvent;
|
||||
|
||||
type GestureSwipeBeginEvent = UnusedEvent;
|
||||
type GestureSwipeUpdateEvent = UnusedEvent;
|
||||
type GestureSwipeEndEvent = UnusedEvent;
|
||||
type GesturePinchBeginEvent = UnusedEvent;
|
||||
type GesturePinchUpdateEvent = UnusedEvent;
|
||||
type GesturePinchEndEvent = UnusedEvent;
|
||||
type GestureHoldBeginEvent = UnusedEvent;
|
||||
type GestureHoldEndEvent = UnusedEvent;
|
||||
|
||||
type TouchDownEvent = UnusedEvent;
|
||||
type TouchUpEvent = UnusedEvent;
|
||||
type TouchMotionEvent = UnusedEvent;
|
||||
type TouchCancelEvent = UnusedEvent;
|
||||
type TouchFrameEvent = UnusedEvent;
|
||||
type TabletToolAxisEvent = UnusedEvent;
|
||||
type TabletToolProximityEvent = UnusedEvent;
|
||||
type TabletToolTipEvent = UnusedEvent;
|
||||
type TabletToolButtonEvent = UnusedEvent;
|
||||
|
||||
type SwitchToggleEvent = UnusedEvent;
|
||||
|
||||
type SpecialEvent = UnusedEvent;
|
||||
}
|
||||
|
||||
pub trait VirtualPointerHandler {
|
||||
fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState;
|
||||
|
||||
fn create_virtual_pointer(&mut self, pointer: VirtualPointer) {
|
||||
let _ = pointer;
|
||||
}
|
||||
fn destroy_virtual_pointer(&mut self, pointer: VirtualPointer) {
|
||||
let _ = pointer;
|
||||
}
|
||||
|
||||
fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent);
|
||||
fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent);
|
||||
fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent);
|
||||
fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent);
|
||||
}
|
||||
|
||||
impl VirtualPointerManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
|
||||
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
|
||||
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||
D: VirtualPointerHandler,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = VirtualPointerManagerGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrVirtualPointerManagerV1, _>(VERSION, global_data);
|
||||
|
||||
Self {
|
||||
virtual_pointers: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData, D>
|
||||
for VirtualPointerManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>,
|
||||
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
|
||||
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||
D: VirtualPointerHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
_state: &mut D,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
manager: New<ZwlrVirtualPointerManagerV1>,
|
||||
_manager_state: &VirtualPointerManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
data_init.init(manager, ());
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &VirtualPointerManagerGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrVirtualPointerManagerV1, (), D> for VirtualPointerManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrVirtualPointerManagerV1, ()>,
|
||||
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||
D: VirtualPointerHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
_client: &Client,
|
||||
_resource: &ZwlrVirtualPointerManagerV1,
|
||||
request: <ZwlrVirtualPointerManagerV1 as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let (id, seat, output) = match request {
|
||||
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => {
|
||||
(id, seat, None)
|
||||
}
|
||||
zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput {
|
||||
seat,
|
||||
output,
|
||||
id,
|
||||
} => (id, seat, output.as_ref().and_then(Output::from_resource)),
|
||||
zwlr_virtual_pointer_manager_v1::Request::Destroy => return,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let pointer = data_init.init(
|
||||
id,
|
||||
VirtualPointerUserData {
|
||||
seat,
|
||||
output,
|
||||
axis_frame: Mutex::new(None),
|
||||
},
|
||||
);
|
||||
state
|
||||
.virtual_pointer_manager_state()
|
||||
.virtual_pointers
|
||||
.insert(pointer.clone());
|
||||
|
||||
state.create_virtual_pointer(VirtualPointer { pointer });
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData, D> for VirtualPointerManagerState
|
||||
where
|
||||
D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>,
|
||||
D: VirtualPointerHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
handler: &mut D,
|
||||
_client: &Client,
|
||||
resource: &ZwlrVirtualPointerV1,
|
||||
request: <ZwlrVirtualPointerV1 as Resource>::Request,
|
||||
_data: &VirtualPointerUserData,
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let pointer = VirtualPointer {
|
||||
pointer: resource.clone(),
|
||||
};
|
||||
match request {
|
||||
zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => {
|
||||
let event = VirtualPointerMotionEvent {
|
||||
pointer,
|
||||
time,
|
||||
dx,
|
||||
dy,
|
||||
};
|
||||
handler.on_virtual_pointer_motion(event);
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::MotionAbsolute {
|
||||
time,
|
||||
x,
|
||||
y,
|
||||
x_extent,
|
||||
y_extent,
|
||||
} => {
|
||||
let event = VirtualPointerMotionAbsoluteEvent {
|
||||
pointer,
|
||||
time,
|
||||
x,
|
||||
y,
|
||||
x_extent,
|
||||
y_extent,
|
||||
};
|
||||
handler.on_virtual_pointer_motion_absolute(event);
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::Button {
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
// state is an enum but wlroots treats it as a C boolean (zero or nonzero)
|
||||
// so we emulate that behaviour too. ButtonState::Pressed and any invalid value
|
||||
// counts as pressed.
|
||||
// https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/3187479c07c34a4de82c06a316a763a36a0499da/types/wlr_virtual_pointer_v1.c#L74
|
||||
let state = match state {
|
||||
WEnum::Value(wl_pointer::ButtonState::Released) => ButtonState::Released,
|
||||
_ => ButtonState::Pressed,
|
||||
};
|
||||
let event = VirtualPointerButtonEvent {
|
||||
pointer,
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
};
|
||||
handler.on_virtual_pointer_button(event);
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => {
|
||||
let axis = match axis {
|
||||
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
|
||||
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
|
||||
_ => {
|
||||
warn!("Axis: invalid axis");
|
||||
resource.post_error(
|
||||
zwlr_virtual_pointer_v1::Error::InvalidAxis,
|
||||
"invalid axis",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
pointer.mutate_axis_frame(Some(time), |frame| frame.value(axis, value));
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::Frame => {
|
||||
if let Some(frame) = pointer.finish_axis_frame() {
|
||||
let event = VirtualPointerAxisEvent { pointer, frame };
|
||||
handler.on_virtual_pointer_axis(event);
|
||||
}
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => {
|
||||
let axis_source = match axis_source {
|
||||
WEnum::Value(wl_pointer::AxisSource::Wheel) => AxisSource::Wheel,
|
||||
WEnum::Value(wl_pointer::AxisSource::Finger) => AxisSource::Finger,
|
||||
WEnum::Value(wl_pointer::AxisSource::Continuous) => AxisSource::Continuous,
|
||||
WEnum::Value(wl_pointer::AxisSource::WheelTilt) => AxisSource::WheelTilt,
|
||||
|
||||
_ => {
|
||||
warn!("AxisSource: invalid axis source");
|
||||
resource.post_error(
|
||||
zwlr_virtual_pointer_v1::Error::InvalidAxisSource,
|
||||
"invalid axis source",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
pointer.mutate_axis_frame(None, |frame| frame.source(axis_source));
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => {
|
||||
let axis = match axis {
|
||||
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
|
||||
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
|
||||
_ => {
|
||||
warn!("AxisStop: invalid axis");
|
||||
resource.post_error(
|
||||
zwlr_virtual_pointer_v1::Error::InvalidAxis,
|
||||
"invalid axis",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
pointer.mutate_axis_frame(Some(time), |frame| frame.stop(axis));
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::AxisDiscrete {
|
||||
time,
|
||||
axis,
|
||||
value,
|
||||
discrete,
|
||||
} => {
|
||||
let axis = match axis {
|
||||
WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical,
|
||||
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal,
|
||||
_ => {
|
||||
warn!("AxisDiscrete: invalid axis");
|
||||
resource.post_error(
|
||||
zwlr_virtual_pointer_v1::Error::InvalidAxis,
|
||||
"invalid axis",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
pointer.mutate_axis_frame(Some(time), |frame| {
|
||||
frame.value(axis, value).v120(axis, discrete)
|
||||
});
|
||||
}
|
||||
zwlr_virtual_pointer_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
handler: &mut D,
|
||||
_client: wayland_backend::server::ClientId,
|
||||
resource: &ZwlrVirtualPointerV1,
|
||||
_data: &VirtualPointerUserData,
|
||||
) {
|
||||
let pointer = VirtualPointer {
|
||||
pointer: resource.clone(),
|
||||
};
|
||||
|
||||
handler.destroy_virtual_pointer(pointer);
|
||||
handler
|
||||
.virtual_pointer_manager_state()
|
||||
.virtual_pointers
|
||||
.remove(resource);
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_virtual_pointer {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: $crate::protocols::virtual_pointer::VirtualPointerManagerGlobalData
|
||||
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: ()
|
||||
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerUserData
|
||||
] => $crate::protocols::virtual_pointer::VirtualPointerManagerState);
|
||||
};
|
||||
}
|
||||
+76
-14
@@ -36,7 +36,7 @@ use smithay::backend::drm::DrmDeviceFd;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::{Output, OutputModeSource, WeakOutput};
|
||||
use smithay::output::{Output, OutputModeSource};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
@@ -44,8 +44,8 @@ use smithay::utils::{Physical, Scale, Size, Transform};
|
||||
use zbus::object_server::SignalEmitter;
|
||||
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode};
|
||||
use crate::niri::State;
|
||||
use crate::render_helpers::render_to_dmabuf;
|
||||
use crate::niri::{CastTarget, State};
|
||||
use crate::render_helpers::{clear_dmabuf, render_to_dmabuf};
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
// Give a 0.1 ms allowance for presentation time errors.
|
||||
@@ -60,16 +60,18 @@ pub struct PipeWire {
|
||||
|
||||
pub enum PwToNiri {
|
||||
StopCast { session_id: usize },
|
||||
Redraw(CastTarget),
|
||||
Redraw { stream_id: usize },
|
||||
FatalError,
|
||||
}
|
||||
|
||||
pub struct Cast {
|
||||
pub session_id: usize,
|
||||
pub stream_id: usize,
|
||||
pub stream: Stream,
|
||||
_listener: StreamListener<()>,
|
||||
pub is_active: Rc<Cell<bool>>,
|
||||
pub target: CastTarget,
|
||||
pub dynamic_target: bool,
|
||||
formats: FormatSet,
|
||||
state: Rc<RefCell<CastState>>,
|
||||
refresh: Rc<Cell<u32>>,
|
||||
@@ -108,12 +110,6 @@ pub enum CastSizeChange {
|
||||
Pending,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum CastTarget {
|
||||
Output(WeakOutput),
|
||||
Window { id: u64 },
|
||||
}
|
||||
|
||||
macro_rules! make_params {
|
||||
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
|
||||
let mut b1 = Vec::new();
|
||||
@@ -189,7 +185,9 @@ impl PipeWire {
|
||||
gbm: GbmDevice<DrmDeviceFd>,
|
||||
formats: FormatSet,
|
||||
session_id: usize,
|
||||
stream_id: usize,
|
||||
target: CastTarget,
|
||||
dynamic_target: bool,
|
||||
size: Size<i32, Physical>,
|
||||
refresh: u32,
|
||||
alpha: bool,
|
||||
@@ -204,10 +202,9 @@ impl PipeWire {
|
||||
warn!("error sending StopCast to niri: {err:?}");
|
||||
}
|
||||
};
|
||||
let target_ = target.clone();
|
||||
let to_niri_ = self.to_niri.clone();
|
||||
let redraw = move || {
|
||||
if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) {
|
||||
if let Err(err) = to_niri_.send(PwToNiri::Redraw { stream_id }) {
|
||||
warn!("error sending Redraw to niri: {err:?}");
|
||||
}
|
||||
};
|
||||
@@ -651,10 +648,12 @@ impl PipeWire {
|
||||
|
||||
let cast = Cast {
|
||||
session_id,
|
||||
stream_id,
|
||||
stream,
|
||||
_listener: listener,
|
||||
is_active,
|
||||
target,
|
||||
dynamic_target,
|
||||
formats,
|
||||
state,
|
||||
refresh,
|
||||
@@ -822,6 +821,7 @@ impl Cast {
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
wait_for_sync: bool,
|
||||
) -> bool {
|
||||
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
|
||||
error!("cast must be in Ready state to render");
|
||||
@@ -852,7 +852,7 @@ impl Cast {
|
||||
let fd = buffer.datas_mut()[0].as_raw().fd;
|
||||
let dmabuf = &self.dmabufs.borrow()[&fd];
|
||||
|
||||
if let Err(err) = render_to_dmabuf(
|
||||
match render_to_dmabuf(
|
||||
renderer,
|
||||
dmabuf.clone(),
|
||||
size,
|
||||
@@ -860,8 +860,70 @@ impl Cast {
|
||||
Transform::Normal,
|
||||
elements.iter().rev(),
|
||||
) {
|
||||
warn!("error rendering to dmabuf: {err:?}");
|
||||
Ok(sync_point) => {
|
||||
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
|
||||
if wait_for_sync {
|
||||
let _span = tracy_client::span!("wait for completion");
|
||||
if let Err(err) = sync_point.wait() {
|
||||
warn!("error waiting for pw frame completion: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error rendering to dmabuf: {err:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (data, (stride, offset)) in
|
||||
zip(buffer.datas_mut(), zip(dmabuf.strides(), dmabuf.offsets()))
|
||||
{
|
||||
let chunk = data.chunk_mut();
|
||||
*chunk.size_mut() = 1;
|
||||
*chunk.stride_mut() = stride as i32;
|
||||
*chunk.offset_mut() = offset;
|
||||
|
||||
trace!(
|
||||
"pw buffer: fd = {}, stride = {stride}, offset = {offset}",
|
||||
data.as_raw().fd
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn dequeue_buffer_and_clear(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
wait_for_sync: bool,
|
||||
) -> bool {
|
||||
// Clear out the damage tracker if we're in Ready state.
|
||||
if let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() {
|
||||
*damage_tracker = None;
|
||||
};
|
||||
|
||||
let Some(mut buffer) = self.stream.dequeue_buffer() else {
|
||||
warn!("no available buffer in pw stream, skipping clear");
|
||||
return false;
|
||||
};
|
||||
|
||||
let fd = buffer.datas_mut()[0].as_raw().fd;
|
||||
let dmabuf = &self.dmabufs.borrow()[&fd];
|
||||
|
||||
match clear_dmabuf(renderer, dmabuf.clone()) {
|
||||
Ok(sync_point) => {
|
||||
// FIXME: implement PipeWire explicit sync, and at the very least async wait.
|
||||
if wait_for_sync {
|
||||
let _span = tracy_client::span!("wait for completion");
|
||||
if let Err(err) = sync_point.wait() {
|
||||
warn!("error waiting for pw frame completion: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error clearing dmabuf: {err:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (data, (stride, offset)) in
|
||||
|
||||
@@ -39,6 +39,7 @@ struct Parameters {
|
||||
corner_radius: CornerRadius,
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
impl BorderRenderElement {
|
||||
@@ -54,6 +55,7 @@ impl BorderRenderElement {
|
||||
border_width: f32,
|
||||
corner_radius: CornerRadius,
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
) -> Self {
|
||||
let inner = ShaderRenderElement::empty(ProgramType::Border, Kind::Unspecified);
|
||||
let mut rv = Self {
|
||||
@@ -69,6 +71,7 @@ impl BorderRenderElement {
|
||||
border_width,
|
||||
corner_radius,
|
||||
scale,
|
||||
alpha,
|
||||
},
|
||||
};
|
||||
rv.update_inner();
|
||||
@@ -90,6 +93,7 @@ impl BorderRenderElement {
|
||||
border_width: 0.,
|
||||
corner_radius: Default::default(),
|
||||
scale: 1.,
|
||||
alpha: 1.,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -111,6 +115,7 @@ impl BorderRenderElement {
|
||||
border_width: f32,
|
||||
corner_radius: CornerRadius,
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
) {
|
||||
let params = Parameters {
|
||||
size,
|
||||
@@ -123,6 +128,7 @@ impl BorderRenderElement {
|
||||
border_width,
|
||||
corner_radius,
|
||||
scale,
|
||||
alpha,
|
||||
};
|
||||
if self.params == params {
|
||||
return;
|
||||
@@ -144,6 +150,7 @@ impl BorderRenderElement {
|
||||
border_width,
|
||||
corner_radius,
|
||||
scale,
|
||||
alpha,
|
||||
} = self.params;
|
||||
|
||||
let grad_offset = geometry.loc - gradient_area.loc;
|
||||
@@ -189,6 +196,7 @@ impl BorderRenderElement {
|
||||
size,
|
||||
None,
|
||||
scale,
|
||||
alpha,
|
||||
vec![
|
||||
Uniform::new("colorspace", colorspace),
|
||||
Uniform::new("hue_interpolation", hue_interpolation),
|
||||
@@ -269,7 +277,7 @@ impl Element for BorderRenderElement {
|
||||
impl RenderElement<GlesRenderer> for BorderRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
@@ -286,7 +294,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
|
||||
@@ -19,9 +19,7 @@ pub struct ClippedSurfaceRenderElement<R: NiriRenderer> {
|
||||
program: GlesTexProgram,
|
||||
corner_radius: CornerRadius,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
input_to_geo: Mat3,
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@@ -72,13 +70,19 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
|
||||
* Mat3::from_scale(buf_size / src_size)
|
||||
* Mat3::from_translation(-src_loc / buf_size);
|
||||
|
||||
let uniforms = vec![
|
||||
Uniform::new("niri_scale", scale.x as f32),
|
||||
Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
|
||||
mat3_uniform("input_to_geo", input_to_geo),
|
||||
];
|
||||
|
||||
Self {
|
||||
inner: elem,
|
||||
program,
|
||||
corner_radius,
|
||||
geometry,
|
||||
input_to_geo,
|
||||
scale: scale.x as f32,
|
||||
uniforms,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,24 +218,13 @@ impl<R: NiriRenderer> Element for ClippedSurfaceRenderElement<R> {
|
||||
impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
frame.override_default_tex_program(
|
||||
self.program.clone(),
|
||||
vec![
|
||||
Uniform::new("niri_scale", self.scale),
|
||||
Uniform::new(
|
||||
"geo_size",
|
||||
(self.geometry.size.w as f32, self.geometry.size.h as f32),
|
||||
),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
|
||||
mat3_uniform("input_to_geo", self.input_to_geo),
|
||||
],
|
||||
);
|
||||
frame.override_default_tex_program(self.program.clone(), self.uniforms.clone());
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
frame.clear_tex_program_override();
|
||||
Ok(())
|
||||
@@ -249,23 +242,15 @@ impl<'render> RenderElement<TtyRenderer<'render>>
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'render, '_>,
|
||||
frame: &mut TtyFrame<'render, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
frame.as_gles_frame().override_default_tex_program(
|
||||
self.program.clone(),
|
||||
vec![
|
||||
Uniform::new(
|
||||
"geo_size",
|
||||
(self.geometry.size.w as f32, self.geometry.size.h as f32),
|
||||
),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
|
||||
mat3_uniform("input_to_geo", self.input_to_geo),
|
||||
],
|
||||
);
|
||||
frame
|
||||
.as_gles_frame()
|
||||
.override_default_tex_program(self.program.clone(), self.uniforms.clone());
|
||||
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
|
||||
frame.as_gles_frame().clear_tex_program_override();
|
||||
Ok(())
|
||||
|
||||
@@ -65,7 +65,7 @@ impl Element for ExtraDamage {
|
||||
impl<R: Renderer> RenderElement<R> for ExtraDamage {
|
||||
fn draw(
|
||||
&self,
|
||||
_frame: &mut <R as Renderer>::Frame<'_>,
|
||||
_frame: &mut R::Frame<'_, '_>,
|
||||
_src: Rectangle<f64, Buffer>,
|
||||
_dst: Rectangle<i32, Physical>,
|
||||
_damage: &[Rectangle<i32, Physical>],
|
||||
|
||||
+49
-18
@@ -6,7 +6,7 @@ use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTarget, GlesTexture};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
@@ -31,6 +31,7 @@ pub mod resize;
|
||||
pub mod resources;
|
||||
pub mod shader_element;
|
||||
pub mod shaders;
|
||||
pub mod shadow;
|
||||
pub mod snapshot;
|
||||
pub mod solid_color;
|
||||
pub mod surface;
|
||||
@@ -156,6 +157,16 @@ impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encompassing_geo(
|
||||
scale: Scale<f64>,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
) -> Rectangle<i32, Physical> {
|
||||
elements
|
||||
.map(|ele| ele.geometry(scale))
|
||||
.reduce(|a, b| a.merge(b))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn render_to_encompassing_texture(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: Scale<f64>,
|
||||
@@ -163,13 +174,9 @@ pub fn render_to_encompassing_texture(
|
||||
fourcc: Fourcc,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
) -> anyhow::Result<(GlesTexture, SyncPoint, Rectangle<i32, Physical>)> {
|
||||
let geo = elements
|
||||
.iter()
|
||||
.map(|ele| ele.geometry(scale))
|
||||
.reduce(|a, b| a.merge(b))
|
||||
.unwrap_or_default();
|
||||
let geo = encompassing_geo(scale, elements.iter());
|
||||
let elements = elements.iter().rev().map(|ele| {
|
||||
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
|
||||
RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative)
|
||||
});
|
||||
|
||||
let (texture, sync_point) =
|
||||
@@ -190,15 +197,18 @@ pub fn render_to_texture(
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
|
||||
let texture: GlesTexture = renderer
|
||||
let mut texture: GlesTexture = renderer
|
||||
.create_buffer(fourcc, buffer_size)
|
||||
.context("error creating texture")?;
|
||||
|
||||
renderer
|
||||
.bind(texture.clone())
|
||||
.context("error binding texture")?;
|
||||
let sync_point = {
|
||||
let mut target = renderer
|
||||
.bind(&mut texture)
|
||||
.context("error binding texture")?;
|
||||
|
||||
render_elements(renderer, &mut target, size, scale, transform, elements)?
|
||||
};
|
||||
|
||||
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
|
||||
Ok((texture, sync_point))
|
||||
}
|
||||
|
||||
@@ -212,11 +222,16 @@ pub fn render_and_download(
|
||||
) -> anyhow::Result<GlesMapping> {
|
||||
let _span = tracy_client::span!();
|
||||
|
||||
let (_, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
|
||||
let (mut texture, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
|
||||
|
||||
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
// FIXME: would be nice to avoid binding the second time here (after render_to_texture()), but
|
||||
// borrowing makes this invonvenient.
|
||||
let target = renderer
|
||||
.bind(&mut texture)
|
||||
.context("error binding texture")?;
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(Rectangle::from_size(buffer_size), fourcc)
|
||||
.copy_framebuffer(&target, Rectangle::from_size(buffer_size), fourcc)
|
||||
.context("error copying framebuffer")?;
|
||||
Ok(mapping)
|
||||
}
|
||||
@@ -241,7 +256,7 @@ pub fn render_to_vec(
|
||||
|
||||
pub fn render_to_dmabuf(
|
||||
renderer: &mut GlesRenderer,
|
||||
dmabuf: Dmabuf,
|
||||
mut dmabuf: Dmabuf,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
@@ -252,8 +267,10 @@ pub fn render_to_dmabuf(
|
||||
dmabuf.width() == size.w as u32 && dmabuf.height() == size.h as u32,
|
||||
"invalid buffer size"
|
||||
);
|
||||
renderer.bind(dmabuf).context("error binding texture")?;
|
||||
render_elements(renderer, size, scale, transform, elements)
|
||||
let mut target = renderer
|
||||
.bind(&mut dmabuf)
|
||||
.context("error binding texture")?;
|
||||
render_elements(renderer, &mut target, size, scale, transform, elements)
|
||||
}
|
||||
|
||||
pub fn render_to_shm(
|
||||
@@ -292,8 +309,22 @@ pub fn render_to_shm(
|
||||
.context("expected shm buffer, but didn't get one")?
|
||||
}
|
||||
|
||||
pub fn clear_dmabuf(renderer: &mut GlesRenderer, mut dmabuf: Dmabuf) -> anyhow::Result<SyncPoint> {
|
||||
let size = dmabuf.size();
|
||||
let size = size.to_logical(1, Transform::Normal).to_physical(1);
|
||||
let mut target = renderer.bind(&mut dmabuf).context("error binding dmabuf")?;
|
||||
let mut frame = renderer
|
||||
.render(&mut target, size, Transform::Normal)
|
||||
.context("error starting frame")?;
|
||||
frame
|
||||
.clear(Color32F::TRANSPARENT, &[Rectangle::from_size(size)])
|
||||
.context("error clearing")?;
|
||||
frame.finish().context("error finishing frame")
|
||||
}
|
||||
|
||||
fn render_elements(
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &mut GlesTarget,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
transform: Transform,
|
||||
@@ -303,7 +334,7 @@ fn render_elements(
|
||||
let output_rect = Rectangle::from_size(transform.transform_size(size));
|
||||
|
||||
let mut frame = renderer
|
||||
.render(size, transform)
|
||||
.render(target, size, transform)
|
||||
.context("error starting frame")?;
|
||||
|
||||
frame
|
||||
|
||||
+281
-177
@@ -1,134 +1,264 @@
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use super::render_to_texture;
|
||||
use super::renderer::AsGlesFrame;
|
||||
use super::texture::{TextureBuffer, TextureRenderElement};
|
||||
use anyhow::Context as _;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{
|
||||
Element, Id, Kind, RenderElement, RenderElementStates, UnderlyingStorage,
|
||||
};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::utils::{
|
||||
CommitCounter, DamageBag, DamageSet, DamageSnapshot, OpaqueRegions,
|
||||
};
|
||||
use smithay::backend::renderer::{
|
||||
Bind as _, Color32F, Frame as _, Offscreen as _, Renderer, Texture as _,
|
||||
};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::encompassing_geo;
|
||||
use super::renderer::AsGlesFrame as _;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Renders elements into an off-screen buffer.
|
||||
/// Buffer for offscreen rendering.
|
||||
#[derive(Debug)]
|
||||
pub struct OffscreenBuffer {
|
||||
id: Id,
|
||||
|
||||
/// The cached texture buffer.
|
||||
///
|
||||
/// Lazily created when `render` is called. Recreated when necessary.
|
||||
inner: RefCell<Option<Inner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Inner {
|
||||
/// The texture with offscreened contents.
|
||||
texture: GlesTexture,
|
||||
/// Id of the renderer that the texture comes from.
|
||||
renderer_id: usize,
|
||||
/// Scale of the texture.
|
||||
scale: Scale<f64>,
|
||||
/// Damage tracker for drawing to the texture.
|
||||
damage: OutputDamageTracker,
|
||||
/// Damage of this offscreen element itself facing outside.
|
||||
outer_damage: DamageBag<i32, Buffer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OffscreenRenderElement {
|
||||
// The texture, if rendering succeeded.
|
||||
texture: Option<PrimaryGpuTextureRenderElement>,
|
||||
// The fallback buffer in case the rendering fails.
|
||||
fallback: SolidColorRenderElement,
|
||||
id: Id,
|
||||
texture: GlesTexture,
|
||||
renderer_id: usize,
|
||||
scale: Scale<f64>,
|
||||
damage: DamageSnapshot<i32, Buffer>,
|
||||
offset: Point<f64, Logical>,
|
||||
src_size: Size<i32, Buffer>,
|
||||
alpha: f32,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OffscreenData {
|
||||
/// Id of the offscreen element.
|
||||
pub id: Id,
|
||||
/// States for the render into the offscreen buffer.
|
||||
pub states: RenderElementStates,
|
||||
}
|
||||
|
||||
impl OffscreenBuffer {
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: Scale<f64>,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
) -> anyhow::Result<(OffscreenRenderElement, SyncPoint, OffscreenData)> {
|
||||
let _span = tracy_client::span!("OffscreenBuffer::render");
|
||||
|
||||
let geo = encompassing_geo(scale, elements.iter());
|
||||
let elements = Vec::from_iter(elements.iter().map(|ele| {
|
||||
RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative)
|
||||
}));
|
||||
|
||||
let src_size = geo.size;
|
||||
let src_size = src_size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||
let offset = geo.loc.to_f64().to_logical(scale);
|
||||
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
// Check if we need to create or recreate the texture.
|
||||
let size_string;
|
||||
let mut reason = "";
|
||||
if let Some(Inner {
|
||||
texture,
|
||||
renderer_id,
|
||||
..
|
||||
}) = inner.as_mut()
|
||||
{
|
||||
let old_size = texture.size();
|
||||
if old_size.w < src_size.w || old_size.h < src_size.h {
|
||||
size_string = format!(
|
||||
"size increased from {} × {} to {} × {}",
|
||||
old_size.w, old_size.h, src_size.w, src_size.h
|
||||
);
|
||||
reason = &size_string;
|
||||
|
||||
*inner = None;
|
||||
} else if !texture.is_unique_reference() {
|
||||
reason = "not unique";
|
||||
|
||||
*inner = None;
|
||||
} else if *renderer_id != renderer.id() {
|
||||
reason = "renderer id changed";
|
||||
|
||||
*inner = None;
|
||||
}
|
||||
} else {
|
||||
reason = "first render";
|
||||
}
|
||||
|
||||
let inner = if let Some(inner) = inner.as_mut() {
|
||||
inner
|
||||
} else {
|
||||
trace!("creating new texture: {reason}");
|
||||
let span = tracy_client::span!("creating offscreen buffer");
|
||||
span.emit_text(reason);
|
||||
|
||||
let texture: GlesTexture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, src_size)
|
||||
.context("error creating texture")?;
|
||||
|
||||
let buffer_size = src_size.to_logical(1, Transform::Normal).to_physical(1);
|
||||
let damage = OutputDamageTracker::new(buffer_size, scale, Transform::Normal);
|
||||
|
||||
inner.insert(Inner {
|
||||
texture,
|
||||
renderer_id: renderer.id(),
|
||||
scale,
|
||||
damage,
|
||||
outer_damage: DamageBag::default(),
|
||||
})
|
||||
};
|
||||
|
||||
// When leaving the old texture as is, its size might be bigger than src_size.
|
||||
let texture_size = inner.texture.size();
|
||||
let buffer_size = texture_size.to_logical(1, Transform::Normal).to_physical(1);
|
||||
|
||||
// Recreate the damage tracker if the scale changes. We already recreate it for buffer size
|
||||
// changes, and transform is always Normal.
|
||||
if inner.scale != scale {
|
||||
inner.scale = scale;
|
||||
|
||||
trace!("recreating damage tracker due to scale change");
|
||||
inner.damage = OutputDamageTracker::new(buffer_size, scale, Transform::Normal);
|
||||
inner.outer_damage = DamageBag::default();
|
||||
}
|
||||
|
||||
let res = {
|
||||
let mut target = renderer.bind(&mut inner.texture)?;
|
||||
inner.damage.render_output(
|
||||
renderer,
|
||||
&mut target,
|
||||
1,
|
||||
&elements,
|
||||
Color32F::TRANSPARENT,
|
||||
)?
|
||||
};
|
||||
|
||||
// Add the resulting damage to the outer tracker.
|
||||
if let Some(damage) = res.damage {
|
||||
// OutputDamageTracker gives us Physical coordinate space, but it's actually the Buffer
|
||||
// space because we were rendering to a texture.
|
||||
let size = buffer_size.to_logical(1);
|
||||
let damage = damage
|
||||
.iter()
|
||||
.map(|rect| rect.to_logical(1).to_buffer(1, Transform::Normal, &size));
|
||||
inner.outer_damage.add(damage);
|
||||
}
|
||||
|
||||
let elem = OffscreenRenderElement {
|
||||
id: self.id.clone(),
|
||||
texture: inner.texture.clone(),
|
||||
renderer_id: inner.renderer_id,
|
||||
scale,
|
||||
damage: inner.outer_damage.snapshot(),
|
||||
offset,
|
||||
src_size,
|
||||
alpha: 1.,
|
||||
kind: Kind::Unspecified,
|
||||
};
|
||||
|
||||
let data = OffscreenData {
|
||||
id: self.id.clone(),
|
||||
states: res.states,
|
||||
};
|
||||
|
||||
Ok((elem, res.sync, data))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OffscreenBuffer {
|
||||
fn default() -> Self {
|
||||
OffscreenBuffer {
|
||||
inner: RefCell::new(None),
|
||||
id: Id::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OffscreenRenderElement {
|
||||
pub fn new(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: i32,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
result_alpha: f32,
|
||||
) -> Self {
|
||||
let _span = tracy_client::span!("OffscreenRenderElement::new");
|
||||
pub fn texture(&self) -> &GlesTexture {
|
||||
&self.texture
|
||||
}
|
||||
|
||||
let geo = elements
|
||||
.iter()
|
||||
.map(|ele| ele.geometry(Scale::from(f64::from(scale))))
|
||||
.reduce(|a, b| a.merge(b))
|
||||
.unwrap_or_default();
|
||||
let logical_size = geo.size.to_logical(scale);
|
||||
pub fn offset(&self) -> Point<f64, Logical> {
|
||||
self.offset
|
||||
}
|
||||
|
||||
let fallback_buffer = SolidColorBuffer::new(logical_size, [1., 0., 0., 1.]);
|
||||
let fallback = SolidColorRenderElement::from_buffer(
|
||||
&fallback_buffer,
|
||||
geo.loc,
|
||||
Scale::from(scale as f64),
|
||||
result_alpha,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
pub fn with_alpha(mut self, alpha: f32) -> Self {
|
||||
self.alpha = alpha;
|
||||
self
|
||||
}
|
||||
|
||||
let elements = elements.iter().rev().map(|ele| {
|
||||
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
|
||||
});
|
||||
pub fn with_offset(mut self, offset: Point<f64, Logical>) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
match render_to_texture(
|
||||
renderer,
|
||||
geo.size,
|
||||
Scale::from(scale as f64),
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements,
|
||||
) {
|
||||
Ok((texture, _sync_point)) => {
|
||||
let buffer = TextureBuffer::from_texture(
|
||||
renderer,
|
||||
texture,
|
||||
scale as f64,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
);
|
||||
let element = TextureRenderElement::from_texture_buffer(
|
||||
buffer,
|
||||
geo.loc.to_f64().to_logical(scale as f64),
|
||||
result_alpha,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
Self {
|
||||
texture: Some(PrimaryGpuTextureRenderElement(element)),
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error off-screening elements: {err:?}");
|
||||
Self {
|
||||
texture: None,
|
||||
fallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn logical_size(&self) -> Size<f64, Logical> {
|
||||
self.src_size
|
||||
.to_f64()
|
||||
.to_logical(self.scale, Transform::Normal)
|
||||
}
|
||||
|
||||
fn damage_since(&self, commit: Option<CommitCounter>) -> DamageSet<i32, Buffer> {
|
||||
self.damage
|
||||
.damage_since(commit)
|
||||
.unwrap_or_else(|| DamageSet::from_slice(&[Rectangle::from_size(self.texture.size())]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for OffscreenRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.id()
|
||||
} else {
|
||||
self.fallback.id()
|
||||
}
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.current_commit()
|
||||
} else {
|
||||
self.fallback.current_commit()
|
||||
}
|
||||
self.damage.current_commit()
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.geometry(scale)
|
||||
} else {
|
||||
self.fallback.geometry(scale)
|
||||
}
|
||||
let logical_geo = Rectangle::new(self.offset, self.logical_size());
|
||||
logical_geo.to_physical_precise_round(scale)
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.transform()
|
||||
} else {
|
||||
self.fallback.transform()
|
||||
}
|
||||
Transform::Normal
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.src()
|
||||
} else {
|
||||
self.fallback.src()
|
||||
}
|
||||
Rectangle::from_size(self.src_size).to_f64()
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
@@ -136,116 +266,90 @@ impl Element for OffscreenRenderElement {
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.damage_since(scale, commit)
|
||||
} else {
|
||||
self.fallback.damage_since(scale, commit)
|
||||
}
|
||||
let texture_size = self.texture.size().to_f64();
|
||||
let src = self.src();
|
||||
|
||||
self.damage_since(commit)
|
||||
.into_iter()
|
||||
.filter_map(|region| {
|
||||
let mut region = region.to_f64().intersection(src)?;
|
||||
|
||||
region.loc -= src.loc;
|
||||
region.upscale(texture_size / src.size);
|
||||
|
||||
let logical = region.to_logical(self.scale, Transform::Normal, &src.size);
|
||||
Some(logical.to_physical_precise_up(scale))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.opaque_regions(scale)
|
||||
} else {
|
||||
self.fallback.opaque_regions(scale)
|
||||
}
|
||||
fn opaque_regions(&self, _scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
OpaqueRegions::default()
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.alpha()
|
||||
} else {
|
||||
self.fallback.alpha()
|
||||
}
|
||||
self.alpha
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.kind()
|
||||
} else {
|
||||
self.fallback.kind()
|
||||
}
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
dest: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
if let Some(texture) = &self.texture {
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
texture,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
)?;
|
||||
} else {
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.fallback,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
)?;
|
||||
if frame.id() != self.renderer_id {
|
||||
warn!("trying to render texture from different renderer");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(())
|
||||
|
||||
frame.render_texture_from_to(
|
||||
&self.texture,
|
||||
src,
|
||||
dest,
|
||||
damage,
|
||||
opaque_regions,
|
||||
Transform::Normal,
|
||||
self.alpha,
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.underlying_storage(renderer)
|
||||
} else {
|
||||
self.fallback.underlying_storage(renderer)
|
||||
}
|
||||
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
let gles_frame = frame.as_gles_frame();
|
||||
if let Some(texture) = &self.texture {
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
texture,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
)?;
|
||||
} else {
|
||||
RenderElement::<GlesRenderer>::draw(
|
||||
&self.fallback,
|
||||
gles_frame,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
)?;
|
||||
}
|
||||
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
|
||||
if let Some(texture) = &self.texture {
|
||||
texture.underlying_storage(renderer)
|
||||
} else {
|
||||
self.fallback.underlying_storage(renderer)
|
||||
}
|
||||
fn underlying_storage(
|
||||
&self,
|
||||
_renderer: &mut TtyRenderer<'render>,
|
||||
) -> Option<UnderlyingStorage> {
|
||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||
// the target GPU into account.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl Element for PrimaryGpuTextureRenderElement {
|
||||
impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
@@ -77,7 +77,7 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
|
||||
@@ -99,7 +99,7 @@ macro_rules! niri_render_elements {
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_>,
|
||||
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
@@ -124,7 +124,7 @@ macro_rules! niri_render_elements {
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut $crate::backend::tty::TtyFrame<'render, '_>,
|
||||
frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>,
|
||||
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
|
||||
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
|
||||
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::{
|
||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, RendererSuper, Texture,
|
||||
};
|
||||
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer};
|
||||
@@ -21,7 +21,7 @@ pub trait NiriRenderer:
|
||||
type NiriError: std::error::Error
|
||||
+ Send
|
||||
+ Sync
|
||||
+ From<<GlesRenderer as Renderer>::Error>
|
||||
+ From<<GlesRenderer as RendererSuper>::Error>
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ impl<R> NiriRenderer for R
|
||||
where
|
||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||
R::TextureId: Texture + Clone + Send + 'static,
|
||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||
R::Error:
|
||||
std::error::Error + Send + Sync + From<<GlesRenderer as RendererSuper>::Error> + 'static,
|
||||
{
|
||||
type NiriTextureId = R::TextureId;
|
||||
type NiriError = R::Error;
|
||||
@@ -53,21 +54,21 @@ impl AsGlesRenderer for TtyRenderer<'_> {
|
||||
}
|
||||
|
||||
/// Trait for getting the underlying `GlesFrame`.
|
||||
pub trait AsGlesFrame<'frame>
|
||||
pub trait AsGlesFrame<'frame, 'buffer>
|
||||
where
|
||||
Self: 'frame,
|
||||
{
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame, 'buffer>;
|
||||
}
|
||||
|
||||
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
impl<'frame, 'buffer> AsGlesFrame<'frame, 'buffer> for GlesFrame<'frame, 'buffer> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame, 'buffer> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'frame> AsGlesFrame<'frame> for TtyFrame<'_, 'frame> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||
impl<'frame, 'buffer> AsGlesFrame<'frame, 'buffer> for TtyFrame<'_, 'frame, 'buffer> {
|
||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame, 'buffer> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use niri_config::CornerRadius;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::backend::renderer::Texture as _;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::{AsGlesFrame, NiriRenderer};
|
||||
@@ -56,10 +57,10 @@ impl ResizeRenderElement {
|
||||
let curr_geo_size = Vec2::new(curr_geo.size.w as f32, curr_geo.size.h as f32);
|
||||
|
||||
let tex_prev_geo_loc = Vec2::new(tex_prev_geo.loc.x as f32, tex_prev_geo.loc.y as f32);
|
||||
let tex_prev_geo_size = Vec2::new(tex_prev_geo.size.w as f32, tex_prev_geo.size.h as f32);
|
||||
let tex_prev_size = Vec2::new(texture_prev.width() as f32, texture_prev.height() as f32);
|
||||
|
||||
let tex_next_geo_loc = Vec2::new(tex_next_geo.loc.x as f32, tex_next_geo.loc.y as f32);
|
||||
let tex_next_geo_size = Vec2::new(tex_next_geo.size.w as f32, tex_next_geo.size.h as f32);
|
||||
let tex_next_size = Vec2::new(texture_next.width() as f32, texture_next.height() as f32);
|
||||
|
||||
let size_prev = Vec2::new(size_prev.w as f32, size_prev.h as f32);
|
||||
let size_next = Vec2::new(size_next.w as f32, size_next.h as f32);
|
||||
@@ -73,10 +74,10 @@ impl ResizeRenderElement {
|
||||
let curr_geo_to_prev_geo = Mat3::from_scale(curr_geo_size / size_prev);
|
||||
let curr_geo_to_next_geo = Mat3::from_scale(curr_geo_size / size_next);
|
||||
|
||||
let geo_to_tex_prev = Mat3::from_translation(-tex_prev_geo_loc / tex_prev_geo_size)
|
||||
* Mat3::from_scale(size_prev / tex_prev_geo_size * scale);
|
||||
let geo_to_tex_next = Mat3::from_translation(-tex_next_geo_loc / tex_next_geo_size)
|
||||
* Mat3::from_scale(size_next / tex_next_geo_size * scale);
|
||||
let geo_to_tex_prev = Mat3::from_translation(-tex_prev_geo_loc / tex_prev_size)
|
||||
* Mat3::from_scale(size_prev / tex_prev_size * scale);
|
||||
let geo_to_tex_next = Mat3::from_translation(-tex_next_geo_loc / tex_next_size)
|
||||
* Mat3::from_scale(size_next / tex_next_size * scale);
|
||||
|
||||
let corner_radius = corner_radius.fit_to(curr_geo_size.x, curr_geo_size.y);
|
||||
let clip_to_geometry = if clip_to_geometry { 1. } else { 0. };
|
||||
@@ -163,7 +164,7 @@ impl Element for ResizeRenderElement {
|
||||
impl RenderElement<GlesRenderer> for ResizeRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
@@ -181,7 +182,7 @@ impl RenderElement<GlesRenderer> for ResizeRenderElement {
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
|
||||
@@ -227,12 +227,14 @@ impl ShaderRenderElement {
|
||||
size: Size<f64, Logical>,
|
||||
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
) {
|
||||
self.area.size = size;
|
||||
self.opaque_regions = opaque_regions.unwrap_or_default();
|
||||
self.scale = scale;
|
||||
self.alpha = alpha;
|
||||
self.additional_uniforms = uniforms;
|
||||
self.textures = textures;
|
||||
|
||||
@@ -243,6 +245,11 @@ impl ShaderRenderElement {
|
||||
self.area.loc = location;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_alpha(mut self, alpha: f32) -> Self {
|
||||
self.alpha = alpha;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ShaderRenderElement {
|
||||
@@ -270,7 +277,7 @@ impl Element for ShaderRenderElement {
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
1.0
|
||||
self.alpha
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
@@ -281,7 +288,7 @@ impl Element for ShaderRenderElement {
|
||||
impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_>,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dest: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
@@ -308,7 +315,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
|
||||
let rect_constrained_loc = rect
|
||||
.loc
|
||||
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
|
||||
.constrain(Rectangle::from_extremities((0, 0), dest_size.to_point()));
|
||||
let rect_clamped_size = rect.size.clamp(
|
||||
(0, 0),
|
||||
(dest_size.to_point() - rect_constrained_loc).to_size(),
|
||||
@@ -328,7 +335,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
|
||||
let rect_constrained_loc = rect
|
||||
.loc
|
||||
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
|
||||
.constrain(Rectangle::from_extremities((0, 0), dest_size.to_point()));
|
||||
let rect_clamped_size = rect.size.clamp(
|
||||
(0, 0),
|
||||
(dest_size.to_point() - rect_constrained_loc).to_size(),
|
||||
@@ -512,7 +519,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for ShaderRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_>,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::shader_element::ShaderProgram;
|
||||
|
||||
pub struct Shaders {
|
||||
pub border: Option<ShaderProgram>,
|
||||
pub shadow: Option<ShaderProgram>,
|
||||
pub clipped_surface: Option<GlesTexProgram>,
|
||||
pub resize: Option<ShaderProgram>,
|
||||
pub custom_resize: RefCell<Option<ShaderProgram>>,
|
||||
@@ -21,6 +22,7 @@ pub struct Shaders {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ProgramType {
|
||||
Border,
|
||||
Shadow,
|
||||
Resize,
|
||||
Close,
|
||||
Open,
|
||||
@@ -53,6 +55,26 @@ impl Shaders {
|
||||
})
|
||||
.ok();
|
||||
|
||||
let shadow = ShaderProgram::compile(
|
||||
renderer,
|
||||
include_str!("shadow.frag"),
|
||||
&[
|
||||
UniformName::new("shadow_color", UniformType::_4f),
|
||||
UniformName::new("sigma", UniformType::_1f),
|
||||
UniformName::new("input_to_geo", UniformType::Matrix3x3),
|
||||
UniformName::new("geo_size", UniformType::_2f),
|
||||
UniformName::new("corner_radius", UniformType::_4f),
|
||||
UniformName::new("window_input_to_geo", UniformType::Matrix3x3),
|
||||
UniformName::new("window_geo_size", UniformType::_2f),
|
||||
UniformName::new("window_corner_radius", UniformType::_4f),
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.map_err(|err| {
|
||||
warn!("error compiling shadow shader: {err:?}");
|
||||
})
|
||||
.ok();
|
||||
|
||||
let clipped_surface = renderer
|
||||
.compile_custom_texture_shader(
|
||||
include_str!("clipped_surface.frag"),
|
||||
@@ -76,6 +98,7 @@ impl Shaders {
|
||||
|
||||
Self {
|
||||
border,
|
||||
shadow,
|
||||
clipped_surface,
|
||||
resize,
|
||||
custom_resize: RefCell::new(None),
|
||||
@@ -84,7 +107,7 @@ impl Shaders {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_from_frame<'a>(frame: &'a mut GlesFrame<'_>) -> &'a Self {
|
||||
pub fn get_from_frame<'a>(frame: &'a mut GlesFrame<'_, '_>) -> &'a Self {
|
||||
let data = frame.egl_context().user_data();
|
||||
data.get()
|
||||
.expect("shaders::init() must be called when creating the renderer")
|
||||
@@ -121,6 +144,7 @@ impl Shaders {
|
||||
pub fn program(&self, program: ProgramType) -> Option<ShaderProgram> {
|
||||
match program {
|
||||
ProgramType::Border => self.border.clone(),
|
||||
ProgramType::Shadow => self.shadow.clone(),
|
||||
ProgramType::Resize => self
|
||||
.custom_resize
|
||||
.borrow()
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
precision highp float;
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
uniform float niri_tint;
|
||||
#endif
|
||||
|
||||
uniform float niri_alpha;
|
||||
uniform float niri_scale;
|
||||
|
||||
uniform vec2 niri_size;
|
||||
varying vec2 niri_v_coords;
|
||||
|
||||
uniform vec4 shadow_color;
|
||||
uniform float sigma;
|
||||
|
||||
uniform mat3 input_to_geo;
|
||||
uniform vec2 geo_size;
|
||||
uniform vec4 corner_radius;
|
||||
|
||||
uniform mat3 window_input_to_geo;
|
||||
uniform vec2 window_geo_size;
|
||||
uniform vec4 window_corner_radius;
|
||||
|
||||
// Based on: https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
|
||||
//
|
||||
// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/)
|
||||
|
||||
// A standard gaussian function, used for weighting samples
|
||||
float gaussian(float x, float sigma) {
|
||||
const float pi = 3.141592653589793;
|
||||
return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma);
|
||||
}
|
||||
|
||||
// This approximates the error function, needed for the gaussian integral
|
||||
vec2 erf(vec2 x) {
|
||||
vec2 s = sign(x), a = abs(x);
|
||||
x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
|
||||
x *= x;
|
||||
return s - s / (x * x);
|
||||
}
|
||||
|
||||
// Return the blurred mask along the x dimension
|
||||
float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) {
|
||||
float delta = min(halfSize.y - corner - abs(y), 0.0);
|
||||
float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
|
||||
vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma));
|
||||
return integral.y - integral.x;
|
||||
}
|
||||
|
||||
// Return the mask for the shadow of a box from lower to upper
|
||||
float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) {
|
||||
// Center everything to make the math easier
|
||||
vec2 center = (lower + upper) * 0.5;
|
||||
vec2 halfSize = (upper - lower) * 0.5;
|
||||
point -= center;
|
||||
|
||||
// The signal is only non-zero in a limited range, so don't waste samples
|
||||
float low = point.y - halfSize.y;
|
||||
float high = point.y + halfSize.y;
|
||||
float start = clamp(-3.0 * sigma, low, high);
|
||||
float end = clamp(3.0 * sigma, low, high);
|
||||
|
||||
// Accumulate samples (we can get away with surprisingly few samples)
|
||||
float step = (end - start) / 4.0;
|
||||
float y = start + step * 0.5;
|
||||
float value = 0.0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step;
|
||||
y += step;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
|
||||
vec2 center;
|
||||
float radius;
|
||||
|
||||
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
|
||||
radius = corner_radius.x;
|
||||
center = vec2(radius, radius);
|
||||
} else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) {
|
||||
radius = corner_radius.y;
|
||||
center = vec2(size.x - radius, radius);
|
||||
} else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) {
|
||||
radius = corner_radius.z;
|
||||
center = vec2(size.x - radius, size.y - radius);
|
||||
} else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) {
|
||||
radius = corner_radius.w;
|
||||
center = vec2(radius, size.y - radius);
|
||||
} else {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float dist = distance(coords, center);
|
||||
float half_px = 0.5 / niri_scale;
|
||||
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0);
|
||||
vec3 coords_window_geo = window_input_to_geo * vec3(niri_v_coords, 1.0);
|
||||
|
||||
vec4 color = shadow_color;
|
||||
|
||||
float shadow_value;
|
||||
if (sigma < 0.1) {
|
||||
// With low enough sigma just draw a rounded rectangle.
|
||||
shadow_value = rounding_alpha(coords_geo.xy, geo_size, corner_radius);
|
||||
} else {
|
||||
shadow_value = roundedBoxShadow(
|
||||
vec2(0.0, 0.0),
|
||||
geo_size,
|
||||
coords_geo.xy,
|
||||
sigma,
|
||||
// FIXME: figure out how to blur with different corner radii.
|
||||
//
|
||||
// GTK seems to call blurring separately for the rect and for the 4 corners:
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gsk/gpu/shaders/gskgpuboxshadow.glsl
|
||||
corner_radius.x
|
||||
);
|
||||
}
|
||||
color = color * shadow_value;
|
||||
|
||||
// Cut out the inside of the window geometry if requested.
|
||||
if (window_geo_size != vec2(0.0, 0.0)) {
|
||||
if (0.0 <= coords_window_geo.x && coords_window_geo.x <= window_geo_size.x
|
||||
&& 0.0 <= coords_window_geo.y && coords_window_geo.y <= window_geo_size.y) {
|
||||
float alpha = rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius);
|
||||
color = color * (1.0 - alpha);
|
||||
}
|
||||
}
|
||||
|
||||
color = color * niri_alpha;
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
if (niri_tint == 1.0)
|
||||
color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8;
|
||||
#endif
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::{Color, CornerRadius};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use super::renderer::NiriRenderer;
|
||||
use super::shader_element::ShaderRenderElement;
|
||||
use super::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Renders a rounded rectangle shadow.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShadowRenderElement {
|
||||
inner: ShaderRenderElement,
|
||||
params: Parameters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct Parameters {
|
||||
size: Size<f64, Logical>,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
color: Color,
|
||||
sigma: f32,
|
||||
corner_radius: CornerRadius,
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
|
||||
window_geometry: Rectangle<f64, Logical>,
|
||||
window_corner_radius: CornerRadius,
|
||||
}
|
||||
|
||||
impl ShadowRenderElement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
size: Size<f64, Logical>,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
color: Color,
|
||||
sigma: f32,
|
||||
corner_radius: CornerRadius,
|
||||
scale: f32,
|
||||
window_geometry: Rectangle<f64, Logical>,
|
||||
window_corner_radius: CornerRadius,
|
||||
alpha: f32,
|
||||
) -> Self {
|
||||
let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified);
|
||||
let mut rv = Self {
|
||||
inner,
|
||||
params: Parameters {
|
||||
size,
|
||||
geometry,
|
||||
color,
|
||||
sigma,
|
||||
corner_radius,
|
||||
scale,
|
||||
alpha,
|
||||
window_geometry,
|
||||
window_corner_radius,
|
||||
},
|
||||
};
|
||||
rv.update_inner();
|
||||
rv
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified);
|
||||
Self {
|
||||
inner,
|
||||
params: Parameters {
|
||||
size: Default::default(),
|
||||
geometry: Default::default(),
|
||||
color: Default::default(),
|
||||
sigma: 0.,
|
||||
corner_radius: Default::default(),
|
||||
scale: 1.,
|
||||
alpha: 1.,
|
||||
window_geometry: Default::default(),
|
||||
window_corner_radius: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn damage_all(&mut self) {
|
||||
self.inner.damage_all();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
size: Size<f64, Logical>,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
color: Color,
|
||||
sigma: f32,
|
||||
corner_radius: CornerRadius,
|
||||
scale: f32,
|
||||
window_geometry: Rectangle<f64, Logical>,
|
||||
window_corner_radius: CornerRadius,
|
||||
alpha: f32,
|
||||
) {
|
||||
let params = Parameters {
|
||||
size,
|
||||
geometry,
|
||||
color,
|
||||
sigma,
|
||||
alpha,
|
||||
corner_radius,
|
||||
scale,
|
||||
window_geometry,
|
||||
window_corner_radius,
|
||||
};
|
||||
if self.params == params {
|
||||
return;
|
||||
}
|
||||
|
||||
self.params = params;
|
||||
self.update_inner();
|
||||
}
|
||||
|
||||
fn update_inner(&mut self) {
|
||||
let Parameters {
|
||||
size,
|
||||
geometry,
|
||||
color,
|
||||
sigma,
|
||||
alpha,
|
||||
corner_radius,
|
||||
scale,
|
||||
window_geometry,
|
||||
window_corner_radius,
|
||||
} = self.params;
|
||||
|
||||
let area_size = Vec2::new(size.w as f32, size.h as f32);
|
||||
|
||||
let geo_loc = Vec2::new(geometry.loc.x as f32, geometry.loc.y as f32);
|
||||
let geo_size = Vec2::new(geometry.size.w as f32, geometry.size.h as f32);
|
||||
|
||||
let input_to_geo =
|
||||
Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size);
|
||||
|
||||
let window_geo_loc = Vec2::new(window_geometry.loc.x as f32, window_geometry.loc.y as f32);
|
||||
let window_geo_size =
|
||||
Vec2::new(window_geometry.size.w as f32, window_geometry.size.h as f32);
|
||||
|
||||
let window_input_to_geo =
|
||||
Mat3::from_scale(area_size) * Mat3::from_translation(-window_geo_loc / area_size);
|
||||
|
||||
self.inner.update(
|
||||
size,
|
||||
None,
|
||||
scale,
|
||||
alpha,
|
||||
vec![
|
||||
Uniform::new("shadow_color", color.to_array_premul()),
|
||||
Uniform::new("sigma", sigma),
|
||||
mat3_uniform("input_to_geo", input_to_geo),
|
||||
Uniform::new("geo_size", geo_size.to_array()),
|
||||
Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)),
|
||||
mat3_uniform("window_input_to_geo", window_input_to_geo),
|
||||
Uniform::new("window_geo_size", window_geo_size.to_array()),
|
||||
Uniform::new(
|
||||
"window_corner_radius",
|
||||
<[f32; 4]>::from(window_corner_radius),
|
||||
),
|
||||
],
|
||||
HashMap::new(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
|
||||
self.inner = self.inner.with_location(location);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_alpha(mut self, alpha: f32) -> Self {
|
||||
self.inner = self.inner.with_alpha(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool {
|
||||
Shaders::get(renderer)
|
||||
.program(ProgramType::Shadow)
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShadowRenderElement {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ShadowRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
self.inner.id()
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.inner.current_commit()
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.inner.geometry(scale)
|
||||
}
|
||||
|
||||
fn transform(&self) -> Transform {
|
||||
self.inner.transform()
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
self.inner.src()
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
scale: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
self.inner.damage_since(scale, commit)
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
self.inner.opaque_regions(scale)
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
self.inner.alpha()
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
self.inner.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for ShadowRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||
self.inner.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'render> RenderElement<TtyRenderer<'render>> for ShadowRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut TtyFrame<'_, '_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), TtyRendererError<'render>> {
|
||||
RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
|
||||
self.inner.underlying_storage(renderer)
|
||||
}
|
||||
}
|
||||
@@ -153,12 +153,12 @@ impl Element for SolidColorRenderElement {
|
||||
impl<R: Renderer> RenderElement<R> for SolidColorRenderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut <R as Renderer>::Frame<'_>,
|
||||
frame: &mut R::Frame<'_, '_>,
|
||||
_src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
_opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), <R as Renderer>::Error> {
|
||||
) -> Result<(), R::Error> {
|
||||
frame.draw_solid(dst, damage, self.color)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::GlesTexture;
|
||||
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
|
||||
use smithay::backend::renderer::{Frame as _, ImportMem, Renderer, Texture};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
@@ -58,7 +59,7 @@ impl<T> TextureBuffer<T> {
|
||||
scale: impl Into<Scale<f64>>,
|
||||
transform: Transform,
|
||||
opaque_regions: Vec<Rectangle<i32, Buffer>>,
|
||||
) -> Result<Self, <R as Renderer>::Error> {
|
||||
) -> Result<Self, R::Error> {
|
||||
let texture = renderer.import_memory(data, format, size.into(), flipped)?;
|
||||
Ok(TextureBuffer::from_texture(
|
||||
renderer,
|
||||
@@ -72,7 +73,7 @@ impl<T> TextureBuffer<T> {
|
||||
pub fn from_memory_buffer<R: Renderer<TextureId = T> + ImportMem>(
|
||||
renderer: &mut R,
|
||||
buffer: &MemoryBuffer,
|
||||
) -> Result<Self, <R as Renderer>::Error> {
|
||||
) -> Result<Self, R::Error> {
|
||||
Self::from_memory(
|
||||
renderer,
|
||||
buffer.data(),
|
||||
@@ -115,6 +116,12 @@ impl<T: Texture> TextureBuffer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl TextureBuffer<GlesTexture> {
|
||||
pub fn is_texture_reference_unique(&mut self) -> bool {
|
||||
self.texture.is_unique_reference()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TextureRenderElement<T> {
|
||||
pub fn from_texture_buffer(
|
||||
buffer: TextureBuffer<T>,
|
||||
@@ -213,12 +220,12 @@ where
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut <R as Renderer>::Frame<'_>,
|
||||
frame: &mut R::Frame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dest: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), <R as Renderer>::Error> {
|
||||
) -> Result<(), R::Error> {
|
||||
if frame.id() != self.buffer.renderer_id {
|
||||
warn!("trying to render texture from different renderer");
|
||||
return Ok(());
|
||||
|
||||
+7
-17
@@ -1,13 +1,11 @@
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::fmt::Write as _;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{env, fmt};
|
||||
|
||||
use calloop::EventLoop;
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
@@ -64,7 +62,7 @@ pub struct Window {
|
||||
pub viewport: WpViewport,
|
||||
pub pending_configure: Configure,
|
||||
pub configures_received: Vec<(u32, Configure)>,
|
||||
pub close_requsted: bool,
|
||||
pub close_requested: bool,
|
||||
|
||||
pub configures_looked_at: usize,
|
||||
}
|
||||
@@ -105,21 +103,13 @@ impl fmt::Display for Configure {
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(socket_name: &OsStr) -> Connection {
|
||||
let mut socket_path = PathBuf::from(env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||
socket_path.push(socket_name);
|
||||
|
||||
let stream = UnixStream::connect(socket_path).unwrap();
|
||||
let backend = Backend::connect(stream).unwrap();
|
||||
Connection::from_backend(backend)
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(socket_name: &OsStr) -> Self {
|
||||
pub fn new(stream: UnixStream) -> Self {
|
||||
let id = ClientId::next();
|
||||
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
let connection = connect(socket_name);
|
||||
let backend = Backend::connect(stream).unwrap();
|
||||
let connection = Connection::from_backend(backend);
|
||||
let queue = connection.new_event_queue();
|
||||
let qh = queue.handle();
|
||||
WaylandSource::new(connection.clone(), queue)
|
||||
@@ -204,7 +194,7 @@ impl State {
|
||||
viewport,
|
||||
pending_configure: Configure::default(),
|
||||
configures_received: Vec::new(),
|
||||
close_requsted: false,
|
||||
close_requested: false,
|
||||
|
||||
configures_looked_at: 0,
|
||||
};
|
||||
@@ -469,7 +459,7 @@ impl Dispatch<XdgToplevel, ()> for State {
|
||||
.collect();
|
||||
}
|
||||
xdg_toplevel::Event::Close => {
|
||||
window.close_requsted = true;
|
||||
window.close_requested = true;
|
||||
}
|
||||
xdg_toplevel::Event::ConfigureBounds { width, height } => {
|
||||
window.pending_configure.bounds = Some((width, height));
|
||||
|
||||
+11
-3
@@ -1,4 +1,5 @@
|
||||
use std::os::fd::AsFd as _;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -9,7 +10,7 @@ use smithay::output::Output;
|
||||
|
||||
use super::client::{Client, ClientId};
|
||||
use super::server::Server;
|
||||
use crate::niri::Niri;
|
||||
use crate::niri::{NewClient, Niri};
|
||||
|
||||
pub struct Fixture {
|
||||
pub event_loop: EventLoop<'static, State>,
|
||||
@@ -88,7 +89,14 @@ impl Fixture {
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self) -> ClientId {
|
||||
let client = Client::new(&self.state.server.state.niri.socket_name);
|
||||
let (sock1, sock2) = UnixStream::pair().unwrap();
|
||||
self.niri().insert_client(NewClient {
|
||||
client: sock1,
|
||||
restricted: false,
|
||||
credentials_unknown: false,
|
||||
});
|
||||
|
||||
let client = Client::new(sock2);
|
||||
let id = client.id;
|
||||
|
||||
let fd = client.event_loop.as_fd().try_clone_to_owned().unwrap();
|
||||
@@ -117,7 +125,7 @@ impl Fixture {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rountrip twice in a row.
|
||||
/// Roundtrip twice in a row.
|
||||
///
|
||||
/// For some reason, when running tests on many threads at once, a single roundtrip is
|
||||
/// sometimes not sufficient to get the configure events to the client.
|
||||
|
||||
@@ -854,3 +854,30 @@ window-rule {
|
||||
@"size: 300 × 100, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfullscreen_to_floating_doesnt_send_extra_configure() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
// Make it floating.
|
||||
f.niri().layout.toggle_window_floating(None);
|
||||
f.roundtrip(id);
|
||||
|
||||
// Fullscreen.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.set_fullscreen(None);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
// Unfullscreen via the window request which requires a configure response.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.unset_fullscreen();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// This should configure only once and not twice.
|
||||
assert_snapshot!(
|
||||
f.client(id).window(&surface).format_recent_configures(),
|
||||
@"size: 936 × 1048, bounds: 1920 × 1080, states: [Activated]"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use client::ClientId;
|
||||
use insta::assert_snapshot;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
|
||||
use super::*;
|
||||
use crate::layout::LayoutElement as _;
|
||||
|
||||
// Sets up a fixture with two outputs and 100×100 window.
|
||||
fn set_up() -> (Fixture, ClientId, WlSurface) {
|
||||
let mut f = Fixture::new();
|
||||
f.add_output(1, (1920, 1080));
|
||||
f.add_output(2, (1280, 720));
|
||||
|
||||
let id = f.add_client();
|
||||
let window = f.client(id).create_window();
|
||||
let surface = window.surface.clone();
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
window.attach_new_buffer();
|
||||
window.set_size(100, 100);
|
||||
window.ack_last_and_commit();
|
||||
f.double_roundtrip(id);
|
||||
|
||||
(f, id, surface)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windowed_fullscreen() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
let niri = f.niri();
|
||||
let mapped = niri.layout.windows().next().unwrap().1;
|
||||
let window_id = mapped.window.clone();
|
||||
|
||||
// Enable windowed fullscreen.
|
||||
niri.layout.toggle_windowed_fullscreen(&window_id);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Should request fullscreen state with the tiled size.
|
||||
let window = f.client(id).window(&surface);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@"size: 936 × 1048, bounds: 1888 × 1048, states: [Activated, Fullscreen]"
|
||||
);
|
||||
|
||||
let mapped = f.niri().layout.windows().next().unwrap().1;
|
||||
// Not committed yet.
|
||||
assert!(!mapped.is_windowed_fullscreen());
|
||||
|
||||
// Commit in response.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let mapped = f.niri().layout.windows().next().unwrap().1;
|
||||
// Now it is committed.
|
||||
assert!(mapped.is_windowed_fullscreen());
|
||||
|
||||
// Disable windowed fullscreen.
|
||||
f.niri().layout.toggle_windowed_fullscreen(&window_id);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Should request without fullscreen state with the tiled size.
|
||||
let window = f.client(id).window(&surface);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@"size: 936 × 1048, bounds: 1888 × 1048, states: [Activated]"
|
||||
);
|
||||
|
||||
let mapped = f.niri().layout.windows().next().unwrap().1;
|
||||
// Not commited yet.
|
||||
assert!(mapped.is_windowed_fullscreen());
|
||||
|
||||
// Commit in response.
|
||||
let window = f.client(id).window(&surface);
|
||||
window.ack_last_and_commit();
|
||||
f.roundtrip(id);
|
||||
|
||||
let mapped = f.niri().layout.windows().next().unwrap().1;
|
||||
// Now it is committed.
|
||||
assert!(!mapped.is_windowed_fullscreen());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windowed_fullscreen_chain() {
|
||||
let (mut f, id, surface) = set_up();
|
||||
|
||||
let _ = f.client(id).window(&surface).recent_configures();
|
||||
|
||||
let mapped = f.niri().layout.windows().next().unwrap().1;
|
||||
let window_id = mapped.window.clone();
|
||||
|
||||
f.niri().layout.toggle_windowed_fullscreen(&window_id);
|
||||
f.roundtrip(id);
|
||||
f.niri().layout.toggle_windowed_fullscreen(&window_id);
|
||||
f.roundtrip(id);
|
||||
f.niri().layout.toggle_windowed_fullscreen(&window_id);
|
||||
f.roundtrip(id);
|
||||
f.niri().layout.toggle_windowed_fullscreen(&window_id);
|
||||
f.double_roundtrip(id);
|
||||
|
||||
// Should be four configures matching the four requests.
|
||||
let window = f.client(id).window(&surface);
|
||||
assert_snapshot!(
|
||||
window.format_recent_configures(),
|
||||
@r"
|
||||
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated, Fullscreen]
|
||||
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated]
|
||||
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated, Fullscreen]
|
||||
size: 936 × 1048, bounds: 1888 × 1048, states: [Activated]
|
||||
"
|
||||
);
|
||||
|
||||
let window = f.client(id).window(&surface);
|
||||
let serials = Vec::from_iter(
|
||||
window.configures_received[window.configures_received.len() - 4..]
|
||||
.iter()
|
||||
.map(|(s, _c)| *s),
|
||||
);
|
||||
|
||||
let get_state = |f: &mut Fixture| {
|
||||
let mapped = f.niri().layout.windows().next().unwrap().1;
|
||||
format!(
|
||||
"fs {}, wfs {}",
|
||||
mapped.is_fullscreen(),
|
||||
mapped.is_windowed_fullscreen()
|
||||
)
|
||||
};
|
||||
|
||||
let mut states = vec![get_state(&mut f)];
|
||||
for serial in serials {
|
||||
let window = f.client(id).window(&surface);
|
||||
window.xdg_surface.ack_configure(serial);
|
||||
window.commit();
|
||||
f.roundtrip(id);
|
||||
states.push(get_state(&mut f));
|
||||
}
|
||||
|
||||
// We expect fs to always be false (because each Fullscreen state request corresponded to a
|
||||
// windowed fullscreen), and wfs to toggle on and off.
|
||||
assert_snapshot!(
|
||||
states.join("\n"),
|
||||
@r"
|
||||
fs false, wfs false
|
||||
fs false, wfs true
|
||||
fs false, wfs false
|
||||
fs false, wfs true
|
||||
fs false, wfs false
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -5,4 +5,5 @@ mod fixture;
|
||||
mod server;
|
||||
|
||||
mod floating;
|
||||
mod fullscreen;
|
||||
mod window_opening;
|
||||
|
||||
@@ -22,6 +22,7 @@ impl Server {
|
||||
event_loop.get_signal(),
|
||||
display,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user