mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-23 02:05:33 +07:00
Compare commits
433 Commits
v0.1.0-beta.1
...
v0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a80078259 | |||
| 303c51ee20 | |||
| 37a836f462 | |||
| 361ede4bcd | |||
| 4fc80124ad | |||
| ba44aeda4a | |||
| b5f7e4bd83 | |||
| b98b95883d | |||
| 568c35ff87 | |||
| c4f600bded | |||
| 2c8d1030ab | |||
| f51dd67f2d | |||
| 3509de6fbf | |||
| 0477986a0d | |||
| 914237fa11 | |||
| 0b93c46ce8 | |||
| 0fcd981b86 | |||
| 5c4153e26b | |||
| 4d010b7943 | |||
| 65c342f2cb | |||
| 47f6c85f64 | |||
| 3b37f1a557 | |||
| dee0abb713 | |||
| bbb4a64126 | |||
| dfe49aa705 | |||
| 7ca39baf9e | |||
| 73e9ef5fe2 | |||
| c40d4f3268 | |||
| 1b496ee21f | |||
| bde46dab52 | |||
| 21ef5aded8 | |||
| b288102866 | |||
| ff42f9b9d3 | |||
| c163e58167 | |||
| a9094b43d4 | |||
| 9e33320b11 | |||
| c40de5364d | |||
| 69f723d68a | |||
| 568fbe26fe | |||
| f8412ecff3 | |||
| 3c6d8062c5 | |||
| 40374942db | |||
| 2c873044e8 | |||
| 1336a581a6 | |||
| 8b0dc1902c | |||
| 9d5f1c7ef7 | |||
| 71be19b234 | |||
| 4fd9300bdb | |||
| 2bb6dd8c48 | |||
| 7319f37f7a | |||
| 0cd149c939 | |||
| 5383a0591f | |||
| 0c68609063 | |||
| 6cd3f96a10 | |||
| 1888696567 | |||
| b9e789619f | |||
| dd011f1012 | |||
| 301a2c0661 | |||
| 956bf7c0a8 | |||
| 209492e700 | |||
| 7e0d3d31f7 | |||
| e448cfb0ef | |||
| 6aceb3a798 | |||
| 4856522a7a | |||
| c1432bfa96 | |||
| ec0531264e | |||
| 03fc439150 | |||
| 83aec41df3 | |||
| 8be9381974 | |||
| dc56f9885c | |||
| 2b3a80b477 | |||
| 294f16f76c | |||
| 4f56ff16f9 | |||
| fe79a6a4e2 | |||
| 950fcf6328 | |||
| 7ff2de19b9 | |||
| f81b51f4c0 | |||
| a90221d924 | |||
| ab22816521 | |||
| 56a55f1ad1 | |||
| f7fde74a8d | |||
| 0470a833a1 | |||
| 092420ec5a | |||
| f46e937949 | |||
| c9a47f8283 | |||
| 9b7ed57d37 | |||
| cf409a4ea6 | |||
| 83bd2317ee | |||
| 0f19003611 | |||
| 470d65a060 | |||
| 4f421907cd | |||
| b4eaaed19e | |||
| d3d178fac7 | |||
| 3091102365 | |||
| a7b3819214 | |||
| 1eff5aeb75 | |||
| 9f0566b1ab | |||
| 3c75082df2 | |||
| 9927c15f68 | |||
| cf87a185a9 | |||
| e276c906bf | |||
| 571768af43 | |||
| c09d5eb048 | |||
| 1a3e31a5cf | |||
| 62f14d42dc | |||
| ce644852d2 | |||
| ffe9a03b58 | |||
| 3c84de5215 | |||
| cd555bbad7 | |||
| 287d9b6b3f | |||
| 9bd812c37a | |||
| 0845eef326 | |||
| 4d8cb3a6e3 | |||
| 48b009ba63 | |||
| addd1f5267 | |||
| b30f8fb2cc | |||
| f5c97faf4a | |||
| 8f1bbea863 | |||
| 5e7eafb2fd | |||
| 41b13aa881 | |||
| fd7f2287f0 | |||
| 1635337504 | |||
| b677592f11 | |||
| ad2795bb27 | |||
| 7826003a81 | |||
| 768fbea14d | |||
| e46003f91f | |||
| 5360ddb320 | |||
| d4b271fead | |||
| de6685f3ab | |||
| 662e2df0e1 | |||
| 26c4824047 | |||
| 78dbb2308e | |||
| 1dce99352e | |||
| 0b6d62f65e | |||
| cf54f75113 | |||
| 0d90876ad8 | |||
| e5bd1113ba | |||
| 6f765db44e | |||
| 5f23d344d5 | |||
| e43e10f44e | |||
| 493c8dc890 | |||
| 8b4a9d68e0 | |||
| a16a0f0e52 | |||
| 6ba195211b | |||
| afaaf36f27 | |||
| f1b36b0dce | |||
| 6ec65bc0d6 | |||
| d65446421f | |||
| 24078cfea2 | |||
| 5cc2c31a5b | |||
| b7ed2fb82a | |||
| f3f02aca20 | |||
| 021a2a1af7 | |||
| 354f0b039a | |||
| d120e0c451 | |||
| 0f724f2011 | |||
| 46131c87a5 | |||
| c66319314e | |||
| b09dbb80c7 | |||
| 54e6a01284 | |||
| 7721e3fc44 | |||
| 0d2fdb49ef | |||
| b06e51da60 | |||
| 6c08ba307a | |||
| 4b2fdd0776 | |||
| 969519b5d8 | |||
| a0c8c39b06 | |||
| 977f1487c2 | |||
| fbe021fbdf | |||
| db49deb7fd | |||
| c61361de3c | |||
| 3963f537a4 | |||
| f31e105043 | |||
| bbb4caeb8c | |||
| d421e1fbf8 | |||
| 23ac3d7323 | |||
| c3327d36da | |||
| e0da101c73 | |||
| 4740682904 | |||
| df9d721f74 | |||
| d970abead8 | |||
| 4f6ed9dfc9 | |||
| 84302796dc | |||
| a39e703fc3 | |||
| a55db6c6c4 | |||
| a011b385d8 | |||
| 2984722f80 | |||
| 118773e17d | |||
| 741bee461c | |||
| 0c57815fbf | |||
| cf89c789c3 | |||
| 642c6e7512 | |||
| 6839a118bb | |||
| 9ae3cad82b | |||
| 89dfaa6cac | |||
| f6ffe8b3ab | |||
| cc83ff008d | |||
| ba4e7481c3 | |||
| c15bc2a028 | |||
| bf1cc98886 | |||
| 5f137b77d3 | |||
| 128d573e74 | |||
| ed8a6afe80 | |||
| 43aa2f95be | |||
| 5c0a1f4d6f | |||
| 8c46611c29 | |||
| 40cec34aa4 | |||
| 1971a41fdd | |||
| 4ea90140d4 | |||
| acd33653b3 | |||
| f7c6516da7 | |||
| b220420fba | |||
| bbeaba16a0 | |||
| 9d7c39b89a | |||
| 03fe864d07 | |||
| e45dbb8ef6 | |||
| 5c4b71a5a4 | |||
| 348690afb6 | |||
| ca22e70cc4 | |||
| 1a784e6e66 | |||
| 3ee2db71a4 | |||
| cedfd4944c | |||
| 431f070481 | |||
| 9cbbffc23c | |||
| c6a1398d51 | |||
| f9127616b0 | |||
| ae89b2e514 | |||
| 732f7f6f33 | |||
| 8bebd54c6d | |||
| 1978e5b0b8 | |||
| 60b02545f3 | |||
| 2750b2038b | |||
| c4145b014a | |||
| 2e51efd3a3 | |||
| caea05433e | |||
| e4f78c26f0 | |||
| 1548db56ce | |||
| 5f416abcf9 | |||
| 66c1272420 | |||
| e0ec6e5b11 | |||
| 93243d7772 | |||
| 24537ec2ba | |||
| 88ac16c99a | |||
| 0add457cf0 | |||
| 6e5426ef22 | |||
| 202406aadf | |||
| 92d9c7ff4f | |||
| 28977d1d3f | |||
| ba10bab010 | |||
| 55038b7c07 | |||
| 8018839f5d | |||
| 077f22edd6 | |||
| 4f7c3300ef | |||
| 5628bf7d77 | |||
| 719697179f | |||
| 5ac350d51c | |||
| 494e98c123 | |||
| ec156a8587 | |||
| e278e871c3 | |||
| ab9d1aab4e | |||
| 506dcd99d7 | |||
| dfbc024127 | |||
| eb2dce1b53 | |||
| f5b776a947 | |||
| 6a587245eb | |||
| 2317021a7c | |||
| af6485cd8c | |||
| f32a25eefe | |||
| aefbad0cf7 | |||
| b091202d86 | |||
| 48f0f6fb3c | |||
| 340bac0690 | |||
| d1b8134337 | |||
| 646e3d8995 | |||
| d1fe6930a7 | |||
| 9e60b344d0 | |||
| 2c01cde9be | |||
| cb9dc9c0cd | |||
| 73d2807b4b | |||
| 7d41f113cb | |||
| 63e5cf8798 | |||
| 9ce19ad7de | |||
| 751f79dc35 | |||
| b8aa0a86e7 | |||
| 82fffdea80 | |||
| 5b3bfd95d9 | |||
| 1a15aa704d | |||
| d58a45a96c | |||
| 9f1b4ee299 | |||
| f0a5e9c933 | |||
| c4c07841d7 | |||
| 6ba24e341f | |||
| 13b6c74cc3 | |||
| d8fb8d5ef0 | |||
| 2b5eeb6162 | |||
| 85be5f746c | |||
| dd7362913e | |||
| 62892d6361 | |||
| 31c13b6a69 | |||
| baaac2f3c4 | |||
| 3fdefae45b | |||
| 6345224e95 | |||
| b3d2096439 | |||
| 94ded2f6a9 | |||
| fa3bc69f94 | |||
| 363e1d8764 | |||
| 8e1d4de0dc | |||
| 72e3fadb9a | |||
| 78cda2e67f | |||
| 924e21f69b | |||
| befdebfa03 | |||
| 7960a73e9d | |||
| 749ee5d627 | |||
| 952dd48115 | |||
| cbd066ab68 | |||
| bccde351fb | |||
| beaffb1b97 | |||
| 385454378b | |||
| 18f06a7acd | |||
| 6e23073019 | |||
| a9fcbf81eb | |||
| a99f34cba8 | |||
| bd2277fa25 | |||
| 67182129ff | |||
| d6b116d229 | |||
| c20a843ab2 | |||
| 1b752fe08f | |||
| 89f74aae98 | |||
| 5e553c2679 | |||
| cabf712821 | |||
| 0931447ec1 | |||
| a388c25795 | |||
| 5c4d9824a4 | |||
| ca4ee5ae25 | |||
| 93e16a6582 | |||
| 3486fa5536 | |||
| c022d74c82 | |||
| e68641c0a7 | |||
| 2a892ef511 | |||
| 90c6721e97 | |||
| e5cd9e9307 | |||
| 573dca10cc | |||
| 577fba82e5 | |||
| b9116c579a | |||
| d8dcadc5b2 | |||
| 6424a2738d | |||
| 753a90430a | |||
| f9085db564 | |||
| 49ce791d13 | |||
| 4b8e04da04 | |||
| 026ad8f377 | |||
| 0761401650 | |||
| 3360517f62 | |||
| 9896fd67a0 | |||
| 15ec699fbb | |||
| a1cc39a437 | |||
| 738d9a2b40 | |||
| 68752db51b | |||
| d4929b8e18 | |||
| 93c547f749 | |||
| e2b91c0c1c | |||
| 322b5cbac7 | |||
| 592791611a | |||
| d073d2ab3d | |||
| b2298db5c5 | |||
| baa6263cbe | |||
| 795da53d53 | |||
| 122afff7d1 | |||
| d2a4e6a0cb | |||
| 8916b18c6b | |||
| b0d0fce5f3 | |||
| 3dc4a5fdac | |||
| 1706a46b2b | |||
| 3789d85588 | |||
| 3a23417e98 | |||
| 6bb83757ee | |||
| b62a07956a | |||
| 96016790b2 | |||
| bf978fe98d | |||
| 57521c69c3 | |||
| da826e42aa | |||
| b824cf90ab | |||
| 7a4bb8ba8a | |||
| 72c8f569ac | |||
| 798d9c55df | |||
| 05613eed1e | |||
| b23dd4b800 | |||
| 1f72089a46 | |||
| fbe9020915 | |||
| 2036116f16 | |||
| 9afd728ae9 | |||
| e51268a39e | |||
| 0a715ce155 | |||
| 89ac958670 | |||
| 2e50f8dee0 | |||
| 7052f0129e | |||
| 962e159db6 | |||
| 11bff3a2f1 | |||
| 15606304f2 | |||
| 85eac9d9d0 | |||
| d3f4583c90 | |||
| fefb1cccd6 | |||
| deef52519a | |||
| 59ff331597 | |||
| b813f99abd | |||
| d9b9cec8b8 | |||
| 597ea62d17 | |||
| 51243a0a50 | |||
| 0ebcc3e0d6 | |||
| 64c85d865e | |||
| 367e4955ea | |||
| dd967554d1 | |||
| 6d7c220137 | |||
| d77aac1afa | |||
| 837a0a20fb | |||
| ecdf756b55 | |||
| 73f3c160b2 | |||
| 5f99eb13ab | |||
| 20326b093c | |||
| 467d92a4b4 | |||
| 15bb69c0b9 | |||
| adfbfdffb3 | |||
| 087ed260c5 | |||
| f5642ab733 | |||
| ab9706cb30 | |||
| 05f2a3709b | |||
| 743173ef64 | |||
| cbbb7a26fc | |||
| 18566e3366 | |||
| df48337d83 | |||
| f5e9b40140 | |||
| 5cacd03e85 |
@@ -0,0 +1 @@
|
|||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report a bug or a crash
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please describe the issue here at the top, then fill in the system information below. -->
|
||||||
|
|
||||||
|
### System Information
|
||||||
|
|
||||||
|
<!-- Paste the output of `niri -V`, e.g. niri 0.1.0-beta.1 (v0.1.0-beta.1) -->
|
||||||
|
* niri version:
|
||||||
|
|
||||||
|
<!-- Write your GPU vendor and model, e.g. AMD RX 6700M -->
|
||||||
|
* GPU:
|
||||||
|
|
||||||
|
<!-- Write your CPU vendor and model, e.g. AMD Ryzen 7 6800H -->
|
||||||
|
* CPU:
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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)
|
||||||
+109
-24
@@ -24,6 +24,7 @@ jobs:
|
|||||||
|
|
||||||
name: test - ${{ matrix.configuration }}
|
name: test - ${{ matrix.configuration }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
container: ubuntu:23.10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -32,34 +33,90 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y software-properties-common
|
apt-get update -y
|
||||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
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
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y 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
|
|
||||||
|
|
||||||
- name: Install Rust
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
run: |
|
|
||||||
rustup set auto-self-update check-only
|
|
||||||
rustup toolchain install stable --profile minimal
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
key: ${{ matrix.configuration }}
|
key: ${{ matrix.configuration }}
|
||||||
|
|
||||||
- name: Build (no default features)
|
- name: Check (no default features)
|
||||||
run: cargo build ${{ matrix.release-flag }} --no-default-features
|
run: cargo check ${{ matrix.release-flag }} --no-default-features
|
||||||
|
|
||||||
- name: Build
|
- name: Check (just dbus)
|
||||||
run: cargo build ${{ matrix.release-flag }}
|
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dbus
|
||||||
|
|
||||||
|
- name: Check (just systemd)
|
||||||
|
run: cargo check ${{ matrix.release-flag }} --no-default-features --features systemd
|
||||||
|
|
||||||
|
- name: Check (just dinit)
|
||||||
|
run: cargo check ${{ matrix.release-flag }} --no-default-features --features dinit
|
||||||
|
|
||||||
|
- name: Check (just xdp-gnome-screencast)
|
||||||
|
run: cargo check ${{ matrix.release-flag }} --no-default-features --features xdp-gnome-screencast
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: cargo check ${{ matrix.release-flag }}
|
||||||
|
|
||||||
- name: Build (with profiling)
|
- name: Build (with profiling)
|
||||||
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
|
||||||
|
|
||||||
- name: Build Tests
|
- name: Build Tests
|
||||||
run: cargo test --no-run --all ${{ matrix.release-flag }}
|
run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
|
run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
|
||||||
|
|
||||||
|
visual-tests:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
name: visual tests
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container: ubuntu:23.10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update -y
|
||||||
|
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 libadwaita-1-dev
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --package niri-visual-tests
|
||||||
|
|
||||||
|
msrv:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
name: 'msrv - 1.72.0'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
container: ubuntu:23.10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update -y
|
||||||
|
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 libadwaita-1-dev
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@1.72.0
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- run: cargo check --all-targets
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -67,6 +124,7 @@ jobs:
|
|||||||
|
|
||||||
name: clippy
|
name: clippy
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
container: ubuntu:23.10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -75,15 +133,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y software-properties-common
|
apt-get update -y
|
||||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
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 libadwaita-1-dev
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y 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
|
|
||||||
|
|
||||||
- name: Install Rust
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
run: |
|
with:
|
||||||
rustup set auto-self-update check-only
|
components: clippy
|
||||||
rustup toolchain install stable --profile minimal --component clippy
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
@@ -119,8 +174,38 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo dnf update -y
|
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
|
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 libadwaita-devel
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo build
|
- run: cargo build --all
|
||||||
|
|
||||||
|
nix:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Check flake inputs
|
||||||
|
uses: DeterminateSystems/flake-checker-action@v4
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@v3
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- run: nix build
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
publish-wiki:
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
needs: build
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
show-progress: false
|
||||||
|
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
|
||||||
|
|||||||
Generated
+870
-415
File diff suppressed because it is too large
Load Diff
+42
-26
@@ -1,5 +1,8 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["niri-visual-tests"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0-beta.1"
|
version = "0.1.5"
|
||||||
description = "A scrollable-tiling Wayland compositor"
|
description = "A scrollable-tiling Wayland compositor"
|
||||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
@@ -7,11 +10,14 @@ edition = "2021"
|
|||||||
repository = "https://github.com/YaLTeR/niri"
|
repository = "https://github.com/YaLTeR/niri"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
bitflags = "2.4.2"
|
anyhow = "1.0.81"
|
||||||
directories = "5.0.1"
|
bitflags = "2.5.0"
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
clap = { version = "~4.4.18", features = ["derive"] }
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
serde_json = "1.0.115"
|
||||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
tracy-client = { version = "0.16.5", default-features = false }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
tracy-client = { version = "0.17.0", default-features = false }
|
||||||
|
|
||||||
[workspace.dependencies.smithay]
|
[workspace.dependencies.smithay]
|
||||||
git = "https://github.com/Smithay/smithay.git"
|
git = "https://github.com/Smithay/smithay.git"
|
||||||
@@ -35,38 +41,41 @@ readme = "README.md"
|
|||||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0.79" }
|
anyhow.workspace = true
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
async-channel = { version = "2.1.1", optional = true }
|
async-channel = { version = "2.2.0", optional = true }
|
||||||
async-io = { version = "1.13.0", optional = true }
|
async-io = { version = "1.13.0", optional = true }
|
||||||
bitflags = "2.4.2"
|
bitflags.workspace = true
|
||||||
calloop = { version = "0.12.4", features = ["executor", "futures-io"] }
|
bytemuck = { version = "1.15.0", features = ["derive"] }
|
||||||
clap = { version = "4.4.18", features = ["derive", "string"] }
|
calloop = { version = "0.13.0", features = ["executor", "futures-io"] }
|
||||||
|
clap = { workspace = true, features = ["string"] }
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
|
drm-ffi = "0.7.1"
|
||||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||||
git-version = "0.3.9"
|
git-version = "0.3.9"
|
||||||
|
glam = "0.27.0"
|
||||||
|
input = { version = "0.9.0", features = ["libinput_1_21"] }
|
||||||
keyframe = { version = "1.1.1", default-features = false }
|
keyframe = { version = "1.1.1", default-features = false }
|
||||||
libc = "0.2.152"
|
libc = "0.2.153"
|
||||||
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
|
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
|
||||||
logind-zbus = { version = "3.1.2", optional = true }
|
niri-config = { version = "0.1.5", path = "niri-config" }
|
||||||
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
|
niri-ipc = { version = "0.1.5", path = "niri-ipc", features = ["clap"] }
|
||||||
niri-ipc = { version = "0.1.0-beta.1", path = "niri-ipc" }
|
|
||||||
notify-rust = { version = "4.10.0", optional = true }
|
notify-rust = { version = "4.10.0", optional = true }
|
||||||
pangocairo = "0.18.0"
|
pangocairo = "0.19.2"
|
||||||
pipewire = { version = "0.7.2", optional = true }
|
pipewire = { version = "0.8.0", optional = true }
|
||||||
png = "0.17.11"
|
png = "0.17.13"
|
||||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||||
profiling = "1.0.13"
|
profiling = "1.0.15"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json = "1.0.111"
|
serde_json.workspace = true
|
||||||
smithay-drm-extras.workspace = true
|
smithay-drm-extras.workspace = true
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracy-client.workspace = true
|
tracy-client.workspace = true
|
||||||
url = { version = "2.5.0", optional = true }
|
url = { version = "2.5.0", optional = true }
|
||||||
xcursor = "0.3.5"
|
xcursor = "0.3.5"
|
||||||
zbus = { version = "3.14.1", optional = true }
|
zbus = { version = "~3.15.2", optional = true }
|
||||||
|
|
||||||
[dependencies.smithay]
|
[dependencies.smithay]
|
||||||
workspace = true
|
workspace = true
|
||||||
@@ -80,6 +89,7 @@ features = [
|
|||||||
"backend_winit",
|
"backend_winit",
|
||||||
"desktop",
|
"desktop",
|
||||||
"renderer_gl",
|
"renderer_gl",
|
||||||
|
"renderer_pixman",
|
||||||
"renderer_multi",
|
"renderer_multi",
|
||||||
"use_system_lib",
|
"use_system_lib",
|
||||||
"wayland_frontend",
|
"wayland_frontend",
|
||||||
@@ -88,15 +98,20 @@ features = [
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.4.0"
|
proptest = "1.4.0"
|
||||||
proptest-derive = "0.4.0"
|
proptest-derive = "0.4.0"
|
||||||
|
xshell = "0.2.5"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["dbus", "xdp-gnome-screencast"]
|
default = ["dbus", "systemd", "xdp-gnome-screencast"]
|
||||||
# Enables DBus support (required for xdp-gnome and power button inhibiting).
|
# Enables D-Bus support (serve various freedesktop and GNOME interfaces, power button handling).
|
||||||
dbus = ["zbus", "logind-zbus", "async-channel", "async-io", "notify-rust", "url"]
|
dbus = ["zbus", "async-channel", "async-io", "notify-rust", "url"]
|
||||||
|
# Enables systemd integration (global environment, apps in transient scopes).
|
||||||
|
systemd = ["dbus"]
|
||||||
# Enables screencasting support through xdg-desktop-portal-gnome.
|
# Enables screencasting support through xdg-desktop-portal-gnome.
|
||||||
xdp-gnome-screencast = ["dbus", "pipewire"]
|
xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||||
# Enables the Tracy profiler instrumentation.
|
# Enables the Tracy profiler instrumentation.
|
||||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||||
|
# Enables dinit integration (global environment).
|
||||||
|
dinit = []
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = "line-tables-only"
|
debug = "line-tables-only"
|
||||||
@@ -108,7 +123,7 @@ lto = "thin"
|
|||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
version = "0.1.0~beta.1"
|
version = "0.1.5"
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||||
@@ -119,3 +134,4 @@ assets = [
|
|||||||
]
|
]
|
||||||
[package.metadata.generate-rpm.requires]
|
[package.metadata.generate-rpm.requires]
|
||||||
alacritty = "*"
|
alacritty = "*"
|
||||||
|
fuzzel = "*"
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|
<p align="center">
|
||||||
|
<a href="https://github.com/YaLTeR/niri/wiki/Getting-Started">Getting Started</a> | <a href="https://github.com/YaLTeR/niri/wiki/Configuration:-Overview">Configuration</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -16,26 +20,35 @@ Opening a new window never causes existing windows to resize.
|
|||||||
Every monitor has its own separate window strip.
|
Every monitor has its own separate window strip.
|
||||||
Windows can never "overflow" onto an adjacent monitor.
|
Windows can never "overflow" onto an adjacent monitor.
|
||||||
|
|
||||||
Since windows go left-to-right horizontally, workspaces are arranged vertically.
|
Workspaces are dynamic and arranged vertically.
|
||||||
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down.
|
||||||
|
|
||||||
|
The workspace arrangement is preserved across disconnecting and connecting monitors where it makes sense.
|
||||||
|
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Scrollable tiling
|
- Scrollable tiling
|
||||||
- Dynamic workspaces like in GNOME
|
- Dynamic workspaces like in GNOME
|
||||||
- Built-in screenshot UI
|
- Built-in screenshot UI
|
||||||
- Monitor screencasting through xdg-desktop-portal-gnome
|
- Monitor screencasting through xdg-desktop-portal-gnome
|
||||||
- Touchpad gesture to switch workspaces
|
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||||
|
- [Touchpad gestures](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515)
|
||||||
- Configurable layout: gaps, borders, struts, window sizes
|
- Configurable layout: gaps, borders, struts, window sizes
|
||||||
|
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e)
|
||||||
- Live-reloading config
|
- Live-reloading config
|
||||||
|
|
||||||
|
## Video Demo
|
||||||
|
|
||||||
|
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
A lot of the essential functionality is implemented, plus some goodies on top.
|
A lot of the essential functionality is implemented, plus some goodies on top.
|
||||||
Feel free to give niri a try.
|
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
||||||
Have your waybars and fuzzels ready: niri is not a complete desktop environment.
|
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||||
|
|
||||||
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
Note that NVIDIA GPUs may have issues.
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
@@ -44,162 +57,11 @@ Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top
|
|||||||
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
|
One of the reasons that prompted me to try writing my own compositor is being able to properly separate the monitors.
|
||||||
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
||||||
|
|
||||||
Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors.
|
|
||||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
|
||||||
>
|
|
||||||
> For NixOS users, check out https://github.com/sodiboo/niri-flake
|
|
||||||
|
|
||||||
First, install the dependencies for your distribution.
|
|
||||||
|
|
||||||
- Ubuntu:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo apt-get install -y software-properties-common
|
|
||||||
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y 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
|
|
||||||
```
|
|
||||||
|
|
||||||
- Fedora:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo dnf install 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
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, build niri with `cargo build --release`.
|
|
||||||
|
|
||||||
### NixOS/Nix
|
|
||||||
|
|
||||||
We have a community-maintained flake which provides a devshell with required dependencies. Use `nix build` to build niri, and then run `./results/bin/niri`.
|
|
||||||
|
|
||||||
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
|
|
||||||
|
|
||||||
```
|
|
||||||
nix run --impure github:guibou/nixGL -- ./results/bin/niri
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
The recommended way to install and run niri is as a standalone desktop session.
|
|
||||||
To do that, put files into the correct directories according to this table.
|
|
||||||
|
|
||||||
| File | Destination |
|
|
||||||
| ---- | ----------- |
|
|
||||||
| `target/release/niri` | `/usr/bin/` |
|
|
||||||
| `resources/niri-session` | `/usr/bin/` |
|
|
||||||
| `resources/niri.desktop` | `/usr/share/wayland-sessions/` |
|
|
||||||
| `resources/niri-portals.conf` | `/usr/share/xdg-desktop-portal/` |
|
|
||||||
| `resources/niri.service` | `/usr/lib/systemd/user/` |
|
|
||||||
| `resources/niri-shutdown.target` | `/usr/lib/systemd/user/` |
|
|
||||||
|
|
||||||
Doing this will make niri appear in GDM and, presumably, other display managers.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
`cargo run --release`
|
|
||||||
|
|
||||||
Inside an existing desktop session, it will run in a window.
|
|
||||||
On a TTY, it will run natively.
|
|
||||||
|
|
||||||
To exit when running on a TTY, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
|
|
||||||
|
|
||||||
### Session
|
|
||||||
|
|
||||||
If you followed the recommended installation steps above, niri should appear in your display manager.
|
|
||||||
Starting it from there will run niri as a desktop session.
|
|
||||||
|
|
||||||
The niri session will autostart apps through the systemd xdg-autostart target.
|
|
||||||
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
|
|
||||||
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
|
|
||||||
|
|
||||||
Niri also works with some parts of xdg-desktop-portal-gnome.
|
|
||||||
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
|
|
||||||
|
|
||||||
[This wiki page](https://github.com/YaLTeR/niri/wiki/Important-Software) explains how to run important software required for normal desktop use, including portals.
|
|
||||||
|
|
||||||
### Xwayland
|
|
||||||
|
|
||||||
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
|
|
||||||
|
|
||||||
### IPC
|
|
||||||
|
|
||||||
You can communicate with the running niri instance over an IPC socket.
|
|
||||||
Check `niri msg --help` for available commands.
|
|
||||||
|
|
||||||
The `--json` flag prints the response in JSON, rather than formatted.
|
|
||||||
For example, `niri msg --json outputs`.
|
|
||||||
|
|
||||||
For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types.
|
|
||||||
The communication over the IPC socket happens in JSON.
|
|
||||||
|
|
||||||
## Default Hotkeys
|
|
||||||
|
|
||||||
When running on a TTY, the Mod key is <kbd>Super</kbd>.
|
|
||||||
When running in a window, the Mod key is <kbd>Alt</kbd>.
|
|
||||||
|
|
||||||
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
|
|
||||||
|
|
||||||
| Hotkey | Description |
|
|
||||||
| ------ | ----------- |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>/</kbd> | Show a list of important niri hotkeys |
|
|
||||||
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` (terminal) |
|
|
||||||
| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` (application launcher) |
|
|
||||||
| <kbd>Mod</kbd><kbd>Alt</kbd><kbd>L</kbd> | Spawn `swaylock` (screen locker) |
|
|
||||||
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
|
|
||||||
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left |
|
|
||||||
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right |
|
|
||||||
| <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>↓</kbd> | Focus the window below in a column |
|
|
||||||
| <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>↑</kbd> | Focus the window above in a column |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>←</kbd> | Move the focused column to the left |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>→</kbd> | Move the focused column to the right |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↓</kbd> | Move the focused window below in a column |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>↑</kbd> | Move the focused window above in a column |
|
|
||||||
| <kbd>Mod</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>End</kbd> | Focus the first or the last column |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Home</kbd> and <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>End</kbd> | Move the focused column to the very start or to the very end |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Focus the monitor to the side |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>H</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>↑</kbd><kbd>→</kbd> | Move the focused column to the monitor to the side |
|
|
||||||
| <kbd>Mod</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>PageDown</kbd> | Switch to the workspace below |
|
|
||||||
| <kbd>Mod</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>PageUp</kbd> | Switch to the workspace above |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageDown</kbd> | Move the focused column to the workspace below |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
|
|
||||||
| <kbd>Mod</kbd><kbd>1</kbd>–<kbd>9</kbd> | Switch to a workspace by index |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>1</kbd>–<kbd>9</kbd> | Move the focused column to a workspace by index |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
|
|
||||||
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
|
|
||||||
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
|
|
||||||
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
|
|
||||||
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
|
|
||||||
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
|
|
||||||
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
|
|
||||||
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>-</kbd> | Decrease window height by 10% |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>=</kbd> | Increase window height by 10% |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>F</kbd> | Toggle full-screen on the focused window |
|
|
||||||
| <kbd>PrtSc</kbd> | Take an area screenshot. Select the area to screenshot with mouse, then press Space to save the screenshot, or Escape to cancel |
|
|
||||||
| <kbd>Alt</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused window to clipboard and to `~/Pictures/Screenshots/` |
|
|
||||||
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
|
|
||||||
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements |
|
|
||||||
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
|
|
||||||
If this fails, it will load [the default configuration file](resources/default-config.kdl).
|
|
||||||
Please use the default configuration file as the starting point for your custom configuration.
|
|
||||||
|
|
||||||
Niri will live-reload most of the configuration settings, like key binds or gaps or output modes, as you change the config file.
|
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||||
|
|
||||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||||
[mako]: https://github.com/emersion/mako
|
[waybar]: https://github.com/Alexays/Waybar
|
||||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||||
|
|
||||||
|
|||||||
Generated
+18
-18
@@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1702918879,
|
"lastModified": 1709610799,
|
||||||
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
|
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
|
"rev": "81c393c776d5379c030607866afef6406ca1be57",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -28,11 +28,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701411808,
|
"lastModified": 1709274179,
|
||||||
"narHash": "sha256-K8QDx8UgbvGdENuvPvcsCXcd8brd55OkRDFLBT7xUVY=",
|
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "3776d0e2a30184cc6a0ba20fb86dc6df5b41fccd",
|
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -47,11 +47,11 @@
|
|||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701680307,
|
"lastModified": 1709126324,
|
||||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -62,11 +62,11 @@
|
|||||||
},
|
},
|
||||||
"nix-filter": {
|
"nix-filter": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701697642,
|
"lastModified": 1705332318,
|
||||||
"narHash": "sha256-L217WytWZHSY8GW9Gx1A64OnNctbuDbfslaTEofXXRw=",
|
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "nix-filter",
|
"repo": "nix-filter",
|
||||||
"rev": "c843418ecfd0344ecb85844b082ff5675e02c443",
|
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -77,11 +77,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1702900294,
|
"lastModified": 1709386671,
|
||||||
"narHash": "sha256-pt7sSoJYNw3n8YtXw0Z/Nnr6/PfY2YrjDvqboErXnRM=",
|
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "886c9aee6ca9324e127f9c2c4e6f68c2641c8256",
|
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -103,11 +103,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1701372675,
|
"lastModified": 1709219524,
|
||||||
"narHash": "sha256-MSHhnAoLjJuoPxzsTzBOzNhjhlCTHPs4nvkPAZVV1eY=",
|
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "c9d189d1375e59a6c9b4d62fdede94ade001f6ee",
|
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -39,22 +39,22 @@
|
|||||||
pname = "niri";
|
pname = "niri";
|
||||||
version = self.rev or "dirty";
|
version = self.rev or "dirty";
|
||||||
|
|
||||||
src = nix-filter.lib.filter {
|
src = nixpkgs.lib.cleanSourceWith {
|
||||||
root = ./.;
|
src = craneLib.path ./.;
|
||||||
include = [
|
filter = path: type:
|
||||||
./src
|
(builtins.match "resources" path == null) ||
|
||||||
./niri-config
|
((craneLib.filterCargoSources path type) &&
|
||||||
./niri-ipc
|
(builtins.match "niri-visual-tests" path == null));
|
||||||
./Cargo.toml
|
|
||||||
./Cargo.lock
|
|
||||||
./resources
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
autoPatchelfHook
|
autoPatchelfHook
|
||||||
clang
|
clang
|
||||||
|
gdk-pixbuf
|
||||||
|
graphene
|
||||||
|
gtk4
|
||||||
|
libadwaita
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags.workspace = true
|
bitflags.workspace = true
|
||||||
|
csscolorparser = "0.6.2"
|
||||||
knuffel = "3.2.0"
|
knuffel = "3.2.0"
|
||||||
miette = "5.10.0"
|
miette = "5.10.0"
|
||||||
smithay.workspace = true
|
niri-ipc = { version = "0.1.5", path = "../niri-ipc" }
|
||||||
|
regex = "1.10.4"
|
||||||
|
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracy-client.workspace = true
|
tracy-client.workspace = true
|
||||||
|
|||||||
+1442
-147
File diff suppressed because it is too large
Load Diff
@@ -8,4 +8,9 @@ edition.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
clap = ["dep:clap"]
|
||||||
|
|||||||
+355
-5
@@ -2,26 +2,243 @@
|
|||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Name of the environment variable containing the niri IPC socket path.
|
mod socket;
|
||||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
pub use socket::{Socket, SOCKET_PATH_ENV};
|
||||||
|
|
||||||
/// Request from client to niri.
|
/// Request from client to niri.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
|
/// Request the version string for the running niri instance.
|
||||||
|
Version,
|
||||||
/// Request information about connected outputs.
|
/// Request information about connected outputs.
|
||||||
Outputs,
|
Outputs,
|
||||||
|
/// Request information about the focused window.
|
||||||
|
FocusedWindow,
|
||||||
|
/// Perform an action.
|
||||||
|
Action(Action),
|
||||||
|
/// Respond with an error (for testing error handling).
|
||||||
|
ReturnError,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response from niri to client.
|
/// Reply from niri to client.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
///
|
||||||
|
/// Every request gets one reply.
|
||||||
|
///
|
||||||
|
/// * If an error had occurred, it will be an `Reply::Err`.
|
||||||
|
/// * If the request does not need any particular response, it will be
|
||||||
|
/// `Reply::Ok(Response::Handled)`. Kind of like an `Ok(())`.
|
||||||
|
/// * Otherwise, it will be `Reply::Ok(response)` with one of the other [`Response`] variants.
|
||||||
|
pub type Reply = Result<Response, String>;
|
||||||
|
|
||||||
|
/// Successful response from niri to client.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
|
/// A request that does not need a response was handled successfully.
|
||||||
|
Handled,
|
||||||
|
/// The version string for the running niri instance.
|
||||||
|
Version(String),
|
||||||
/// Information about connected outputs.
|
/// Information about connected outputs.
|
||||||
///
|
///
|
||||||
/// Map from connector name to output info.
|
/// Map from connector name to output info.
|
||||||
Outputs(HashMap<String, Output>),
|
Outputs(HashMap<String, Output>),
|
||||||
|
/// Information about the focused window.
|
||||||
|
FocusedWindow(Option<Window>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "clap", derive(clap::Parser))]
|
||||||
|
#[cfg_attr(feature = "clap", command(subcommand_value_name = "ACTION"))]
|
||||||
|
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Actions"))]
|
||||||
|
pub enum Action {
|
||||||
|
/// Exit niri.
|
||||||
|
Quit {
|
||||||
|
/// Skip the "Press Enter to confirm" prompt.
|
||||||
|
#[cfg_attr(feature = "clap", arg(short, long))]
|
||||||
|
skip_confirmation: bool,
|
||||||
|
},
|
||||||
|
/// Power off all monitors via DPMS.
|
||||||
|
PowerOffMonitors,
|
||||||
|
/// Spawn a command.
|
||||||
|
Spawn {
|
||||||
|
/// Command to spawn.
|
||||||
|
#[cfg_attr(feature = "clap", arg(last = true, required = true))]
|
||||||
|
command: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Open the screenshot UI.
|
||||||
|
Screenshot,
|
||||||
|
/// Screenshot the focused screen.
|
||||||
|
ScreenshotScreen,
|
||||||
|
/// Screenshot the focused window.
|
||||||
|
ScreenshotWindow,
|
||||||
|
/// Close the focused window.
|
||||||
|
CloseWindow,
|
||||||
|
/// Toggle fullscreen on the focused window.
|
||||||
|
FullscreenWindow,
|
||||||
|
/// Focus the column to the left.
|
||||||
|
FocusColumnLeft,
|
||||||
|
/// Focus the column to the right.
|
||||||
|
FocusColumnRight,
|
||||||
|
/// Focus the first column.
|
||||||
|
FocusColumnFirst,
|
||||||
|
/// Focus the last column.
|
||||||
|
FocusColumnLast,
|
||||||
|
/// Focus the window below.
|
||||||
|
FocusWindowDown,
|
||||||
|
/// Focus the window above.
|
||||||
|
FocusWindowUp,
|
||||||
|
/// Focus the window or the workspace above.
|
||||||
|
FocusWindowOrWorkspaceDown,
|
||||||
|
/// Focus the window or the workspace above.
|
||||||
|
FocusWindowOrWorkspaceUp,
|
||||||
|
/// Move the focused column to the left.
|
||||||
|
MoveColumnLeft,
|
||||||
|
/// Move the focused column to the right.
|
||||||
|
MoveColumnRight,
|
||||||
|
/// Move the focused column to the start of the workspace.
|
||||||
|
MoveColumnToFirst,
|
||||||
|
/// Move the focused column to the end of the workspace.
|
||||||
|
MoveColumnToLast,
|
||||||
|
/// Move the focused window down in a column.
|
||||||
|
MoveWindowDown,
|
||||||
|
/// Move the focused window up in a column.
|
||||||
|
MoveWindowUp,
|
||||||
|
/// Move the focused window down in a column or to the workspace below.
|
||||||
|
MoveWindowDownOrToWorkspaceDown,
|
||||||
|
/// Move the focused window up in a column or to the workspace above.
|
||||||
|
MoveWindowUpOrToWorkspaceUp,
|
||||||
|
/// Consume or expel the focused window left.
|
||||||
|
ConsumeOrExpelWindowLeft,
|
||||||
|
/// Consume or expel the focused window right.
|
||||||
|
ConsumeOrExpelWindowRight,
|
||||||
|
/// Consume the window to the right into the focused column.
|
||||||
|
ConsumeWindowIntoColumn,
|
||||||
|
/// Expel the focused window from the column.
|
||||||
|
ExpelWindowFromColumn,
|
||||||
|
/// Center the focused column on the screen.
|
||||||
|
CenterColumn,
|
||||||
|
/// Focus the workspace below.
|
||||||
|
FocusWorkspaceDown,
|
||||||
|
/// Focus the workspace above.
|
||||||
|
FocusWorkspaceUp,
|
||||||
|
/// Focus a workspace by index.
|
||||||
|
FocusWorkspace {
|
||||||
|
/// Index of the workspace to focus.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: u8,
|
||||||
|
},
|
||||||
|
/// Focus the previous workspace.
|
||||||
|
FocusWorkspacePrevious,
|
||||||
|
/// Move the focused window to the workspace below.
|
||||||
|
MoveWindowToWorkspaceDown,
|
||||||
|
/// Move the focused window to the workspace above.
|
||||||
|
MoveWindowToWorkspaceUp,
|
||||||
|
/// Move the focused window to a workspace by index.
|
||||||
|
MoveWindowToWorkspace {
|
||||||
|
/// Index of the target workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: u8,
|
||||||
|
},
|
||||||
|
/// Move the focused column to the workspace below.
|
||||||
|
MoveColumnToWorkspaceDown,
|
||||||
|
/// Move the focused column to the workspace above.
|
||||||
|
MoveColumnToWorkspaceUp,
|
||||||
|
/// Move the focused column to a workspace by index.
|
||||||
|
MoveColumnToWorkspace {
|
||||||
|
/// Index of the target workspace.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
index: u8,
|
||||||
|
},
|
||||||
|
/// Move the focused workspace down.
|
||||||
|
MoveWorkspaceDown,
|
||||||
|
/// Move the focused workspace up.
|
||||||
|
MoveWorkspaceUp,
|
||||||
|
/// Focus the monitor to the left.
|
||||||
|
FocusMonitorLeft,
|
||||||
|
/// Focus the monitor to the right.
|
||||||
|
FocusMonitorRight,
|
||||||
|
/// Focus the monitor below.
|
||||||
|
FocusMonitorDown,
|
||||||
|
/// Focus the monitor above.
|
||||||
|
FocusMonitorUp,
|
||||||
|
/// Move the focused window to the monitor to the left.
|
||||||
|
MoveWindowToMonitorLeft,
|
||||||
|
/// Move the focused window to the monitor to the right.
|
||||||
|
MoveWindowToMonitorRight,
|
||||||
|
/// Move the focused window to the monitor below.
|
||||||
|
MoveWindowToMonitorDown,
|
||||||
|
/// Move the focused window to the monitor above.
|
||||||
|
MoveWindowToMonitorUp,
|
||||||
|
/// Move the focused column to the monitor to the left.
|
||||||
|
MoveColumnToMonitorLeft,
|
||||||
|
/// Move the focused column to the monitor to the right.
|
||||||
|
MoveColumnToMonitorRight,
|
||||||
|
/// Move the focused column to the monitor below.
|
||||||
|
MoveColumnToMonitorDown,
|
||||||
|
/// Move the focused column to the monitor above.
|
||||||
|
MoveColumnToMonitorUp,
|
||||||
|
/// Change the height of the focused window.
|
||||||
|
SetWindowHeight {
|
||||||
|
/// How to change the height.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
change: SizeChange,
|
||||||
|
},
|
||||||
|
/// Switch between preset column widths.
|
||||||
|
SwitchPresetColumnWidth,
|
||||||
|
/// Toggle the maximized state of the focused column.
|
||||||
|
MaximizeColumn,
|
||||||
|
/// Change the width of the focused column.
|
||||||
|
SetColumnWidth {
|
||||||
|
/// How to change the width.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
change: SizeChange,
|
||||||
|
},
|
||||||
|
/// Switch between keyboard layouts.
|
||||||
|
SwitchLayout {
|
||||||
|
/// Layout to switch to.
|
||||||
|
#[cfg_attr(feature = "clap", arg())]
|
||||||
|
layout: LayoutSwitchTarget,
|
||||||
|
},
|
||||||
|
/// Show the hotkey overlay.
|
||||||
|
ShowHotkeyOverlay,
|
||||||
|
/// Move the focused workspace to the monitor to the left.
|
||||||
|
MoveWorkspaceToMonitorLeft,
|
||||||
|
/// Move the focused workspace to the monitor to the right.
|
||||||
|
MoveWorkspaceToMonitorRight,
|
||||||
|
/// Move the focused workspace to the monitor below.
|
||||||
|
MoveWorkspaceToMonitorDown,
|
||||||
|
/// Move the focused workspace to the monitor above.
|
||||||
|
MoveWorkspaceToMonitorUp,
|
||||||
|
/// Toggle a debug tint on windows.
|
||||||
|
ToggleDebugTint,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change in window or column size.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum SizeChange {
|
||||||
|
/// Set the size in logical pixels.
|
||||||
|
SetFixed(i32),
|
||||||
|
/// Set the size as a proportion of the working area.
|
||||||
|
SetProportion(f64),
|
||||||
|
/// Add or subtract to the current size in logical pixels.
|
||||||
|
AdjustFixed(i32),
|
||||||
|
/// Add or subtract to the current size as a proportion of the working area.
|
||||||
|
AdjustProportion(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout to switch to.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LayoutSwitchTarget {
|
||||||
|
/// The next configured layout.
|
||||||
|
Next,
|
||||||
|
/// The previous configured layout.
|
||||||
|
Prev,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connected output.
|
/// Connected output.
|
||||||
@@ -41,6 +258,14 @@ pub struct Output {
|
|||||||
///
|
///
|
||||||
/// `None` if the output is disabled.
|
/// `None` if the output is disabled.
|
||||||
pub current_mode: Option<usize>,
|
pub current_mode: Option<usize>,
|
||||||
|
/// Whether the output supports variable refresh rate.
|
||||||
|
pub vrr_supported: bool,
|
||||||
|
/// Whether variable refresh rate is enabled on the output.
|
||||||
|
pub vrr_enabled: bool,
|
||||||
|
/// Logical output information.
|
||||||
|
///
|
||||||
|
/// `None` if the output is not mapped to any logical output (for example, if it is disabled).
|
||||||
|
pub logical: Option<LogicalOutput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Output mode.
|
/// Output mode.
|
||||||
@@ -52,4 +277,129 @@ pub struct Mode {
|
|||||||
pub height: u16,
|
pub height: u16,
|
||||||
/// Refresh rate in millihertz.
|
/// Refresh rate in millihertz.
|
||||||
pub refresh_rate: u32,
|
pub refresh_rate: u32,
|
||||||
|
/// Whether this mode is preferred by the monitor.
|
||||||
|
pub is_preferred: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logical output in the compositor's coordinate space.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub struct LogicalOutput {
|
||||||
|
/// Logical X position.
|
||||||
|
pub x: i32,
|
||||||
|
/// Logical Y position.
|
||||||
|
pub y: i32,
|
||||||
|
/// Width in logical pixels.
|
||||||
|
pub width: u32,
|
||||||
|
/// Height in logical pixels.
|
||||||
|
pub height: u32,
|
||||||
|
/// Scale factor.
|
||||||
|
pub scale: f64,
|
||||||
|
/// Transform.
|
||||||
|
pub transform: Transform,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output transform, which goes counter-clockwise.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Transform {
|
||||||
|
/// Untransformed.
|
||||||
|
Normal,
|
||||||
|
/// Rotated by 90°.
|
||||||
|
#[serde(rename = "90")]
|
||||||
|
_90,
|
||||||
|
/// Rotated by 180°.
|
||||||
|
#[serde(rename = "180")]
|
||||||
|
_180,
|
||||||
|
/// Rotated by 270°.
|
||||||
|
#[serde(rename = "270")]
|
||||||
|
_270,
|
||||||
|
/// Flipped horizontally.
|
||||||
|
Flipped,
|
||||||
|
/// Rotated by 90° and flipped horizontally.
|
||||||
|
Flipped90,
|
||||||
|
/// Flipped vertically.
|
||||||
|
Flipped180,
|
||||||
|
/// Rotated by 270° and flipped horizontally.
|
||||||
|
Flipped270,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toplevel window.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Window {
|
||||||
|
/// Title, if set.
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// Application ID, if set.
|
||||||
|
pub app_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SizeChange {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.split_once('%') {
|
||||||
|
Some((value, empty)) => {
|
||||||
|
if !empty.is_empty() {
|
||||||
|
return Err("trailing characters after '%' are not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.bytes().next() {
|
||||||
|
Some(b'-' | b'+') => {
|
||||||
|
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||||
|
Ok(Self::AdjustProportion(value))
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||||
|
Ok(Self::SetProportion(value))
|
||||||
|
}
|
||||||
|
None => Err("value is missing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let value = s;
|
||||||
|
match value.bytes().next() {
|
||||||
|
Some(b'-' | b'+') => {
|
||||||
|
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||||
|
Ok(Self::AdjustFixed(value))
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
let value = value.parse().map_err(|_| "error parsing value")?;
|
||||||
|
Ok(Self::SetFixed(value))
|
||||||
|
}
|
||||||
|
None => Err("value is missing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for LayoutSwitchTarget {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"next" => Ok(Self::Next),
|
||||||
|
"prev" => Ok(Self::Prev),
|
||||||
|
_ => Err(r#"invalid layout action, can be "next" or "prev""#),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Transform {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"normal" => Ok(Self::Normal),
|
||||||
|
"90" => Ok(Self::_90),
|
||||||
|
"180" => Ok(Self::_180),
|
||||||
|
"270" => Ok(Self::_270),
|
||||||
|
"flipped" => Ok(Self::Flipped),
|
||||||
|
"flipped-90" => Ok(Self::Flipped90),
|
||||||
|
"flipped-180" => Ok(Self::Flipped180),
|
||||||
|
"flipped-270" => Ok(Self::Flipped270),
|
||||||
|
_ => Err(concat!(
|
||||||
|
r#"invalid transform, can be "90", "180", "270", "#,
|
||||||
|
r#""flipped", "flipped-90", "flipped-180" or "flipped-270""#
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//! Helper for blocking communication over the niri socket.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::net::Shutdown;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::{Reply, Request};
|
||||||
|
|
||||||
|
/// Name of the environment variable containing the niri IPC socket path.
|
||||||
|
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||||
|
|
||||||
|
/// Helper for blocking communication over the niri socket.
|
||||||
|
///
|
||||||
|
/// This struct is used to communicate with the niri IPC server. It handles the socket connection
|
||||||
|
/// and serialization/deserialization of messages.
|
||||||
|
pub struct Socket {
|
||||||
|
stream: UnixStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Socket {
|
||||||
|
/// Connects to the default niri IPC socket.
|
||||||
|
///
|
||||||
|
/// This is equivalent to calling [`Self::connect_to`] with the path taken from the
|
||||||
|
/// [`SOCKET_PATH_ENV`] environment variable.
|
||||||
|
pub fn connect() -> io::Result<Self> {
|
||||||
|
let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Self::connect_to(socket_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to the niri IPC socket at the given path.
|
||||||
|
pub fn connect_to(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||||
|
let stream = UnixStream::connect(path.as_ref())?;
|
||||||
|
Ok(Self { stream })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a request to niri and returns the response.
|
||||||
|
///
|
||||||
|
/// Return values:
|
||||||
|
///
|
||||||
|
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||||
|
/// * `Ok(Err(message))`: error message from niri
|
||||||
|
/// * `Err(error)`: error communicating with niri
|
||||||
|
pub fn send(self, request: Request) -> io::Result<Reply> {
|
||||||
|
let Self { mut stream } = self;
|
||||||
|
|
||||||
|
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||||
|
stream.write_all(&buf)?;
|
||||||
|
stream.shutdown(Shutdown::Write)?;
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
stream.read_to_end(&mut buf)?;
|
||||||
|
|
||||||
|
let reply = serde_json::from_slice(&buf)?;
|
||||||
|
Ok(reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "niri-visual-tests"
|
||||||
|
version.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
|
||||||
|
anyhow.workspace = true
|
||||||
|
gtk = { version = "0.8.1", package = "gtk4", features = ["v4_12"] }
|
||||||
|
niri = { version = "0.1.5", path = ".." }
|
||||||
|
niri-config = { version = "0.1.5", path = "../niri-config" }
|
||||||
|
smithay.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# niri-visual-tests
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>
|
||||||
|
> This is a development-only app, you shouldn't package it.
|
||||||
|
|
||||||
|
This app contains a number of hard-coded test scenarios for visual inspection.
|
||||||
|
It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
|
||||||
|
The idea is to go through the test scenarios and check that everything *looks* right.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
You will need recent GTK and libadwaita.
|
||||||
|
Then, `cargo run`.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.anim-control-bar {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use std::f32::consts::{FRAC_PI_2, PI};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri::animation::ANIMATION_SLOWDOWN;
|
||||||
|
use niri::render_helpers::gradient::GradientRenderElement;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::utils::{Logical, Physical, Rectangle, Scale, Size};
|
||||||
|
|
||||||
|
use super::TestCase;
|
||||||
|
|
||||||
|
pub struct GradientAngle {
|
||||||
|
angle: f32,
|
||||||
|
prev_time: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GradientAngle {
|
||||||
|
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||||
|
Self {
|
||||||
|
angle: 0.,
|
||||||
|
prev_time: Duration::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase for GradientAngle {
|
||||||
|
fn are_animations_ongoing(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_animations(&mut self, current_time: Duration) {
|
||||||
|
let mut delta = if self.prev_time.is_zero() {
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
current_time.saturating_sub(self.prev_time)
|
||||||
|
};
|
||||||
|
self.prev_time = current_time;
|
||||||
|
|
||||||
|
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||||
|
if slowdown == 0. {
|
||||||
|
delta = Duration::ZERO
|
||||||
|
} else {
|
||||||
|
delta = delta.div_f64(slowdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.angle += delta.as_secs_f32() * PI;
|
||||||
|
|
||||||
|
if self.angle >= PI * 2. {
|
||||||
|
self.angle -= PI * 2.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
|
let (a, b) = (size.w / 4, size.h / 4);
|
||||||
|
let size = (size.w - a * 2, size.h - b * 2);
|
||||||
|
let area = Rectangle::from_loc_and_size((a, b), size);
|
||||||
|
|
||||||
|
GradientRenderElement::new(
|
||||||
|
renderer,
|
||||||
|
Scale::from(1.),
|
||||||
|
area,
|
||||||
|
area,
|
||||||
|
[1., 0., 0., 1.],
|
||||||
|
[0., 1., 0., 1.],
|
||||||
|
self.angle - FRAC_PI_2,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|elem| Box::new(elem) as _)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use std::f32::consts::{FRAC_PI_4, PI};
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri::animation::ANIMATION_SLOWDOWN;
|
||||||
|
use niri::layout::focus_ring::FocusRing;
|
||||||
|
use niri::render_helpers::gradient::GradientRenderElement;
|
||||||
|
use niri_config::Color;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size};
|
||||||
|
|
||||||
|
use super::TestCase;
|
||||||
|
|
||||||
|
pub struct GradientArea {
|
||||||
|
progress: f32,
|
||||||
|
border: FocusRing,
|
||||||
|
prev_time: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GradientArea {
|
||||||
|
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut border = FocusRing::new(niri_config::FocusRing {
|
||||||
|
off: false,
|
||||||
|
width: 1,
|
||||||
|
active_color: Color::new(255, 255, 255, 128),
|
||||||
|
inactive_color: Color::default(),
|
||||||
|
active_gradient: None,
|
||||||
|
inactive_gradient: None,
|
||||||
|
});
|
||||||
|
border.set_active(true);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
progress: 0.,
|
||||||
|
border,
|
||||||
|
prev_time: Duration::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase for GradientArea {
|
||||||
|
fn are_animations_ongoing(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_animations(&mut self, current_time: Duration) {
|
||||||
|
let mut delta = if self.prev_time.is_zero() {
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
current_time.saturating_sub(self.prev_time)
|
||||||
|
};
|
||||||
|
self.prev_time = current_time;
|
||||||
|
|
||||||
|
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::SeqCst);
|
||||||
|
if slowdown == 0. {
|
||||||
|
delta = Duration::ZERO
|
||||||
|
} else {
|
||||||
|
delta = delta.div_f64(slowdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progress += delta.as_secs_f32() * PI;
|
||||||
|
|
||||||
|
if self.progress >= PI * 2. {
|
||||||
|
self.progress -= PI * 2.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
|
let mut rv = Vec::new();
|
||||||
|
|
||||||
|
let f = (self.progress.sin() + 1.) / 2.;
|
||||||
|
|
||||||
|
let (a, b) = (size.w / 4, size.h / 4);
|
||||||
|
let rect_size = (size.w - a * 2, size.h - b * 2);
|
||||||
|
let area = Rectangle::from_loc_and_size((a, b), rect_size);
|
||||||
|
|
||||||
|
let g_size = Size::from((
|
||||||
|
(size.w as f32 / 8. + size.w as f32 / 8. * 7. * f).round() as i32,
|
||||||
|
(size.h as f32 / 8. + size.h as f32 / 8. * 7. * f).round() as i32,
|
||||||
|
));
|
||||||
|
let g_loc = ((size.w - g_size.w) / 2, (size.h - g_size.h) / 2);
|
||||||
|
let g_area = Rectangle::from_loc_and_size(g_loc, g_size);
|
||||||
|
|
||||||
|
self.border.update(g_size, true);
|
||||||
|
rv.extend(
|
||||||
|
self.border
|
||||||
|
.render(
|
||||||
|
renderer,
|
||||||
|
Point::from(g_loc),
|
||||||
|
Scale::from(1.),
|
||||||
|
size.to_logical(1),
|
||||||
|
)
|
||||||
|
.map(|elem| Box::new(elem) as _),
|
||||||
|
);
|
||||||
|
|
||||||
|
rv.extend(
|
||||||
|
GradientRenderElement::new(
|
||||||
|
renderer,
|
||||||
|
Scale::from(1.),
|
||||||
|
area,
|
||||||
|
g_area,
|
||||||
|
[1., 0., 0., 1.],
|
||||||
|
[0., 1., 0., 1.],
|
||||||
|
FRAC_PI_4,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|elem| Box::new(elem) as _),
|
||||||
|
);
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri::layout::workspace::ColumnWidth;
|
||||||
|
use niri::layout::{LayoutElement as _, Options};
|
||||||
|
use niri::render_helpers::RenderTarget;
|
||||||
|
use niri::utils::get_monotonic_time;
|
||||||
|
use niri_config::Color;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::desktop::layer_map_for_output;
|
||||||
|
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
|
||||||
|
use smithay::utils::{Logical, Physical, Size};
|
||||||
|
|
||||||
|
use super::TestCase;
|
||||||
|
use crate::test_window::TestWindow;
|
||||||
|
|
||||||
|
type DynStepFn = Box<dyn FnOnce(&mut Layout)>;
|
||||||
|
|
||||||
|
pub struct Layout {
|
||||||
|
output: Output,
|
||||||
|
windows: Vec<TestWindow>,
|
||||||
|
layout: niri::layout::Layout<TestWindow>,
|
||||||
|
start_time: Duration,
|
||||||
|
steps: HashMap<Duration, DynStepFn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn new(size: Size<i32, Logical>) -> Self {
|
||||||
|
let output = Output::new(
|
||||||
|
String::new(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: Size::from((size.w, size.h)),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: String::new(),
|
||||||
|
model: String::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mode = Some(Mode {
|
||||||
|
size: size.to_physical(1),
|
||||||
|
refresh: 60000,
|
||||||
|
});
|
||||||
|
output.change_current_state(mode, None, None, None);
|
||||||
|
|
||||||
|
let options = Options {
|
||||||
|
focus_ring: niri_config::FocusRing {
|
||||||
|
off: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
border: niri_config::Border {
|
||||||
|
off: false,
|
||||||
|
width: 4,
|
||||||
|
active_color: Color::new(255, 163, 72, 255),
|
||||||
|
inactive_color: Color::new(50, 50, 50, 255),
|
||||||
|
active_gradient: None,
|
||||||
|
inactive_gradient: None,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut layout = niri::layout::Layout::with_options(options);
|
||||||
|
layout.add_output(output.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
output,
|
||||||
|
windows: Vec::new(),
|
||||||
|
layout,
|
||||||
|
start_time: get_monotonic_time(),
|
||||||
|
steps: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_in_between(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::new(size);
|
||||||
|
|
||||||
|
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||||
|
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::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.layout.start_open_animation_for_window(win.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::new(size);
|
||||||
|
|
||||||
|
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.layout.start_open_animation_for_window(win.id());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::new(size);
|
||||||
|
|
||||||
|
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.layout.start_open_animation_for_window(win.id());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_to_the_left(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::new(size);
|
||||||
|
|
||||||
|
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||||
|
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::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.layout.start_open_animation_for_window(win.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::new(size);
|
||||||
|
|
||||||
|
rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3)));
|
||||||
|
rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::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.layout.start_open_animation_for_window(win.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
||||||
|
let ws = self.layout.active_workspace().unwrap();
|
||||||
|
window.request_size(ws.new_window_size(width), false);
|
||||||
|
window.communicate();
|
||||||
|
|
||||||
|
self.layout.add_window(window.clone(), width, false);
|
||||||
|
self.windows.push(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_window_right_of(
|
||||||
|
&mut self,
|
||||||
|
right_of: &TestWindow,
|
||||||
|
mut window: TestWindow,
|
||||||
|
width: Option<ColumnWidth>,
|
||||||
|
) {
|
||||||
|
let ws = self.layout.active_workspace().unwrap();
|
||||||
|
window.request_size(ws.new_window_size(width), false);
|
||||||
|
window.communicate();
|
||||||
|
|
||||||
|
self.layout
|
||||||
|
.add_window_right_of(right_of.id(), window.clone(), width, false);
|
||||||
|
self.windows.push(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) {
|
||||||
|
self.steps
|
||||||
|
.insert(Duration::from_millis(delay_ms), Box::new(f) as _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase for Layout {
|
||||||
|
fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
let mode = Some(Mode {
|
||||||
|
size: Size::from((width, height)),
|
||||||
|
refresh: 60000,
|
||||||
|
});
|
||||||
|
self.output.change_current_state(mode, None, None, None);
|
||||||
|
layer_map_for_output(&self.output).arrange();
|
||||||
|
self.layout.update_output_size(&self.output);
|
||||||
|
for win in &self.windows {
|
||||||
|
if win.communicate() {
|
||||||
|
self.layout.update_window(win.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.layout
|
||||||
|
.monitor_for_output(&self.output)
|
||||||
|
.unwrap()
|
||||||
|
.are_animations_ongoing()
|
||||||
|
|| !self.steps.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_animations(&mut self, mut current_time: Duration) {
|
||||||
|
let run = self
|
||||||
|
.steps
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.filter(|delay| self.start_time + *delay <= current_time)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for key in &run {
|
||||||
|
let f = self.steps.remove(key).unwrap();
|
||||||
|
f(self);
|
||||||
|
}
|
||||||
|
if !run.is_empty() {
|
||||||
|
current_time = get_monotonic_time();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layout.advance_animations(current_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
_size: Size<i32, Physical>,
|
||||||
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
|
self.layout
|
||||||
|
.monitor_for_output(&self.output)
|
||||||
|
.unwrap()
|
||||||
|
.render_elements(renderer, RenderTarget::Output)
|
||||||
|
.into_iter()
|
||||||
|
.map(|elem| Box::new(elem) as _)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::utils::{Physical, Size};
|
||||||
|
|
||||||
|
pub mod gradient_angle;
|
||||||
|
pub mod gradient_area;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod tile;
|
||||||
|
pub mod window;
|
||||||
|
|
||||||
|
pub trait TestCase {
|
||||||
|
fn resize(&mut self, _width: i32, _height: i32) {}
|
||||||
|
fn are_animations_ongoing(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn advance_animations(&mut self, _current_time: Duration) {}
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri::layout::Options;
|
||||||
|
use niri::render_helpers::RenderTarget;
|
||||||
|
use niri_config::Color;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||||
|
|
||||||
|
use super::TestCase;
|
||||||
|
use crate::test_window::TestWindow;
|
||||||
|
|
||||||
|
pub struct Tile {
|
||||||
|
window: TestWindow,
|
||||||
|
tile: niri::layout::tile::Tile<TestWindow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tile {
|
||||||
|
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||||
|
let window = TestWindow::freeform(0);
|
||||||
|
let mut rv = Self::with_window(window);
|
||||||
|
rv.tile.request_tile_size(size, false);
|
||||||
|
rv.window.communicate();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||||
|
let window = TestWindow::fixed_size(0);
|
||||||
|
let mut rv = Self::with_window(window);
|
||||||
|
rv.tile.request_tile_size(size, false);
|
||||||
|
rv.window.communicate();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||||
|
let window = TestWindow::fixed_size(0);
|
||||||
|
window.set_csd_shadow_width(64);
|
||||||
|
let mut rv = Self::with_window(window);
|
||||||
|
rv.tile.request_tile_size(size, false);
|
||||||
|
rv.window.communicate();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn freeform_open(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::freeform(size);
|
||||||
|
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||||
|
rv.tile.start_open_animation();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size_open(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::fixed_size(size);
|
||||||
|
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||||
|
rv.tile.start_open_animation();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut rv = Self::fixed_size_with_csd_shadow(size);
|
||||||
|
rv.window.set_color([0.1, 0.1, 0.1, 1.]);
|
||||||
|
rv.tile.start_open_animation();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_window(window: TestWindow) -> Self {
|
||||||
|
let options = Options {
|
||||||
|
focus_ring: niri_config::FocusRing {
|
||||||
|
off: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
border: niri_config::Border {
|
||||||
|
off: false,
|
||||||
|
width: 32,
|
||||||
|
active_color: Color::new(255, 163, 72, 255),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let tile = niri::layout::tile::Tile::new(window.clone(), Rc::new(options));
|
||||||
|
Self { window, tile }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase for Tile {
|
||||||
|
fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
self.tile
|
||||||
|
.request_tile_size(Size::from((width, height)), false);
|
||||||
|
self.window.communicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.tile.are_animations_ongoing()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_animations(&mut self, current_time: Duration) {
|
||||||
|
self.tile.advance_animations(current_time, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
|
let tile_size = self.tile.tile_size().to_physical(1);
|
||||||
|
let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
|
||||||
|
|
||||||
|
self.tile
|
||||||
|
.render(
|
||||||
|
renderer,
|
||||||
|
location,
|
||||||
|
Scale::from(1.),
|
||||||
|
size.to_logical(1),
|
||||||
|
true,
|
||||||
|
RenderTarget::Output,
|
||||||
|
)
|
||||||
|
.map(|elem| Box::new(elem) as _)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use niri::layout::LayoutElement;
|
||||||
|
use niri::render_helpers::RenderTarget;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::utils::{Logical, Physical, Point, Scale, Size};
|
||||||
|
|
||||||
|
use super::TestCase;
|
||||||
|
use crate::test_window::TestWindow;
|
||||||
|
|
||||||
|
pub struct Window {
|
||||||
|
window: TestWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut window = TestWindow::freeform(0);
|
||||||
|
window.request_size(size, false);
|
||||||
|
window.communicate();
|
||||||
|
Self { window }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut window = TestWindow::fixed_size(0);
|
||||||
|
window.request_size(size, false);
|
||||||
|
window.communicate();
|
||||||
|
Self { window }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||||
|
let mut window = TestWindow::fixed_size(0);
|
||||||
|
window.set_csd_shadow_width(64);
|
||||||
|
window.request_size(size, false);
|
||||||
|
window.communicate();
|
||||||
|
Self { window }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase for Window {
|
||||||
|
fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
self.window.request_size(Size::from((width, height)), false);
|
||||||
|
self.window.communicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||||
|
let win_size = self.window.size().to_physical(1);
|
||||||
|
let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
|
||||||
|
|
||||||
|
self.window
|
||||||
|
.render(
|
||||||
|
renderer,
|
||||||
|
location,
|
||||||
|
Scale::from(1.),
|
||||||
|
1.,
|
||||||
|
RenderTarget::Output,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|elem| Box::new(elem) as _)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate tracing;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
|
||||||
|
use cases::tile::Tile;
|
||||||
|
use cases::window::Window;
|
||||||
|
use gtk::prelude::{
|
||||||
|
AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
|
||||||
|
};
|
||||||
|
use gtk::{gdk, gio, glib};
|
||||||
|
use niri::animation::ANIMATION_SLOWDOWN;
|
||||||
|
use smithay::utils::{Logical, Size};
|
||||||
|
use smithay_view::SmithayView;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use crate::cases::gradient_angle::GradientAngle;
|
||||||
|
use crate::cases::gradient_area::GradientArea;
|
||||||
|
use crate::cases::layout::Layout;
|
||||||
|
use crate::cases::TestCase;
|
||||||
|
|
||||||
|
mod cases;
|
||||||
|
mod smithay_view;
|
||||||
|
mod test_window;
|
||||||
|
|
||||||
|
fn main() -> glib::ExitCode {
|
||||||
|
let directives =
|
||||||
|
env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
|
||||||
|
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.compact()
|
||||||
|
.with_env_filter(env_filter)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
|
||||||
|
app.connect_startup(on_startup);
|
||||||
|
app.connect_activate(build_ui);
|
||||||
|
app.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_startup(_app: &adw::Application) {
|
||||||
|
// Load our CSS.
|
||||||
|
let provider = gtk::CssProvider::new();
|
||||||
|
provider.load_from_string(include_str!("../resources/style.css"));
|
||||||
|
if let Some(display) = gdk::Display::default() {
|
||||||
|
gtk::style_context_add_provider_for_display(
|
||||||
|
&display,
|
||||||
|
&provider,
|
||||||
|
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(app: &adw::Application) {
|
||||||
|
let stack = gtk::Stack::new();
|
||||||
|
|
||||||
|
struct S {
|
||||||
|
stack: gtk::Stack,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl S {
|
||||||
|
fn add<T: TestCase + 'static>(
|
||||||
|
&self,
|
||||||
|
make: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||||
|
title: &str,
|
||||||
|
) {
|
||||||
|
let view = SmithayView::new(make);
|
||||||
|
self.stack.add_titled(&view, None, title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = S {
|
||||||
|
stack: stack.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
s.add(Window::freeform, "Freeform Window");
|
||||||
|
s.add(Window::fixed_size, "Fixed Size Window");
|
||||||
|
s.add(
|
||||||
|
Window::fixed_size_with_csd_shadow,
|
||||||
|
"Fixed Size Window - CSD Shadow",
|
||||||
|
);
|
||||||
|
|
||||||
|
s.add(Tile::freeform, "Freeform Tile");
|
||||||
|
s.add(Tile::fixed_size, "Fixed Size Tile");
|
||||||
|
s.add(
|
||||||
|
Tile::fixed_size_with_csd_shadow,
|
||||||
|
"Fixed Size Tile - CSD Shadow",
|
||||||
|
);
|
||||||
|
s.add(Tile::freeform_open, "Freeform Tile - Open");
|
||||||
|
s.add(Tile::fixed_size_open, "Fixed Size Tile - Open");
|
||||||
|
s.add(
|
||||||
|
Tile::fixed_size_with_csd_shadow_open,
|
||||||
|
"Fixed Size Tile - CSD Shadow - Open",
|
||||||
|
);
|
||||||
|
|
||||||
|
s.add(Layout::open_in_between, "Layout - Open In-Between");
|
||||||
|
s.add(
|
||||||
|
Layout::open_multiple_quickly,
|
||||||
|
"Layout - Open Multiple Quickly",
|
||||||
|
);
|
||||||
|
s.add(
|
||||||
|
Layout::open_multiple_quickly_big,
|
||||||
|
"Layout - Open Multiple Quickly - Big",
|
||||||
|
);
|
||||||
|
s.add(Layout::open_to_the_left, "Layout - Open To The Left");
|
||||||
|
s.add(
|
||||||
|
Layout::open_to_the_left_big,
|
||||||
|
"Layout - Open To The Left - Big",
|
||||||
|
);
|
||||||
|
|
||||||
|
s.add(GradientAngle::new, "Gradient - Angle");
|
||||||
|
s.add(GradientArea::new, "Gradient - Area");
|
||||||
|
|
||||||
|
let content_headerbar = adw::HeaderBar::new();
|
||||||
|
|
||||||
|
let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
|
||||||
|
anim_adjustment
|
||||||
|
.connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
|
||||||
|
let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
|
||||||
|
anim_scale.set_hexpand(true);
|
||||||
|
|
||||||
|
let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||||
|
anim_control_bar.add_css_class("anim-control-bar");
|
||||||
|
anim_control_bar.append(>k::Label::new(Some("Slowdown")));
|
||||||
|
anim_control_bar.append(&anim_scale);
|
||||||
|
|
||||||
|
let content_view = adw::ToolbarView::new();
|
||||||
|
content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
|
||||||
|
content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
|
||||||
|
content_view.add_top_bar(&content_headerbar);
|
||||||
|
content_view.add_bottom_bar(&anim_control_bar);
|
||||||
|
content_view.set_content(Some(&stack));
|
||||||
|
let content = adw::NavigationPage::new(
|
||||||
|
&content_view,
|
||||||
|
stack
|
||||||
|
.page(&stack.visible_child().unwrap())
|
||||||
|
.title()
|
||||||
|
.as_deref()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let sidebar_header = adw::HeaderBar::new();
|
||||||
|
let stack_sidebar = gtk::StackSidebar::new();
|
||||||
|
stack_sidebar.set_stack(&stack);
|
||||||
|
let sidebar_view = adw::ToolbarView::new();
|
||||||
|
sidebar_view.add_top_bar(&sidebar_header);
|
||||||
|
sidebar_view.set_content(Some(&stack_sidebar));
|
||||||
|
let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
|
||||||
|
|
||||||
|
let split_view = adw::NavigationSplitView::new();
|
||||||
|
split_view.set_content(Some(&content));
|
||||||
|
split_view.set_sidebar(Some(&sidebar));
|
||||||
|
|
||||||
|
stack.connect_visible_child_notify(move |stack| {
|
||||||
|
content.set_title(
|
||||||
|
stack
|
||||||
|
.visible_child()
|
||||||
|
.and_then(|c| stack.page(&c).title())
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let window = adw::ApplicationWindow::new(app);
|
||||||
|
window.set_title(Some("niri visual tests"));
|
||||||
|
window.set_content(Some(&split_view));
|
||||||
|
window.present();
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
use gtk::glib;
|
||||||
|
use gtk::subclass::prelude::*;
|
||||||
|
use smithay::utils::{Logical, Size};
|
||||||
|
|
||||||
|
use crate::cases::TestCase;
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use std::cell::{Cell, OnceCell, RefCell};
|
||||||
|
use std::ptr::null;
|
||||||
|
|
||||||
|
use anyhow::{ensure, Context};
|
||||||
|
use gtk::gdk;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use niri::render_helpers::{resources, shaders};
|
||||||
|
use niri::utils::get_monotonic_time;
|
||||||
|
use smithay::backend::egl::ffi::egl;
|
||||||
|
use smithay::backend::egl::EGLContext;
|
||||||
|
use smithay::backend::renderer::gles::{Capability, GlesRenderer};
|
||||||
|
use smithay::backend::renderer::{Frame, Renderer, Unbind};
|
||||||
|
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SmithayView {
|
||||||
|
gl_area: gtk::GLArea,
|
||||||
|
size: Cell<(i32, i32)>,
|
||||||
|
renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
|
||||||
|
pub make_test_case: OnceCell<DynMakeTestCase>,
|
||||||
|
test_case: RefCell<Option<Box<dyn TestCase>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for SmithayView {
|
||||||
|
const NAME: &'static str = "NiriSmithayView";
|
||||||
|
type Type = super::SmithayView;
|
||||||
|
type ParentType = gtk::Widget;
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
klass.set_layout_manager_type::<gtk::BinLayout>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for SmithayView {
|
||||||
|
fn constructed(&self) {
|
||||||
|
let obj = self.obj();
|
||||||
|
|
||||||
|
self.parent_constructed();
|
||||||
|
|
||||||
|
self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
|
||||||
|
self.gl_area.set_parent(&*obj);
|
||||||
|
|
||||||
|
self.gl_area.connect_resize({
|
||||||
|
let imp = self.downgrade();
|
||||||
|
move |_, width, height| {
|
||||||
|
if let Some(imp) = imp.upgrade() {
|
||||||
|
imp.resize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.gl_area.connect_render({
|
||||||
|
let imp = self.downgrade();
|
||||||
|
move |_, gl_context| {
|
||||||
|
if let Some(imp) = imp.upgrade() {
|
||||||
|
if let Err(err) = imp.render(gl_context) {
|
||||||
|
warn!("error rendering: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj.add_tick_callback(|obj, _frame_clock| {
|
||||||
|
let imp = obj.imp();
|
||||||
|
|
||||||
|
if let Some(case) = &mut *imp.test_case.borrow_mut() {
|
||||||
|
if case.are_animations_ongoing() {
|
||||||
|
imp.gl_area.queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispose(&self) {
|
||||||
|
self.gl_area.unparent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetImpl for SmithayView {
|
||||||
|
fn unmap(&self) {
|
||||||
|
self.test_case.replace(None);
|
||||||
|
self.parent_unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unrealize(&self) {
|
||||||
|
self.renderer.replace(None);
|
||||||
|
self.parent_unrealize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmithayView {
|
||||||
|
fn resize(&self, width: i32, height: i32) {
|
||||||
|
self.size.set((width, height));
|
||||||
|
|
||||||
|
if let Some(case) = &mut *self.test_case.borrow_mut() {
|
||||||
|
case.resize(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
|
||||||
|
// Set up the Smithay renderer.
|
||||||
|
let mut renderer = self.renderer.borrow_mut();
|
||||||
|
let renderer = renderer.get_or_insert_with(|| {
|
||||||
|
unsafe { create_renderer() }
|
||||||
|
.map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
|
||||||
|
});
|
||||||
|
let Ok(renderer) = renderer else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = self.size.get();
|
||||||
|
|
||||||
|
// Create the test case if missing.
|
||||||
|
let mut case = self.test_case.borrow_mut();
|
||||||
|
let case = case.get_or_insert_with(|| {
|
||||||
|
let make = self.make_test_case.get().unwrap();
|
||||||
|
make(Size::from(size))
|
||||||
|
});
|
||||||
|
|
||||||
|
case.advance_animations(get_monotonic_time());
|
||||||
|
|
||||||
|
let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
|
||||||
|
|
||||||
|
let elements = unsafe {
|
||||||
|
with_framebuffer_save_restore(renderer, |renderer| {
|
||||||
|
case.render(renderer, Size::from(size))
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let mut frame = renderer
|
||||||
|
.render(rect.size, Transform::Normal)
|
||||||
|
.context("error creating frame")?;
|
||||||
|
|
||||||
|
frame
|
||||||
|
.clear([0.3, 0.3, 0.3, 1.], &[rect])
|
||||||
|
.context("error clearing")?;
|
||||||
|
|
||||||
|
for element in elements.iter().rev() {
|
||||||
|
let src = element.src();
|
||||||
|
let dst = element.geometry(Scale::from(1.));
|
||||||
|
|
||||||
|
if let Some(mut damage) = rect.intersection(dst) {
|
||||||
|
damage.loc -= dst.loc;
|
||||||
|
element
|
||||||
|
.draw(&mut frame, src, dst, &[damage])
|
||||||
|
.context("error drawing element")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
|
||||||
|
smithay::backend::egl::ffi::make_sure_egl_is_loaded()
|
||||||
|
.context("error loading EGL symbols in Smithay")?;
|
||||||
|
|
||||||
|
let egl_display = egl::GetCurrentDisplay();
|
||||||
|
ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
|
||||||
|
|
||||||
|
let egl_context = egl::GetCurrentContext();
|
||||||
|
ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
|
||||||
|
|
||||||
|
// There's no config ID on the EGL context and there's no current EGL surface, but we don't
|
||||||
|
// really use it anyway so just get some random one.
|
||||||
|
let mut egl_config_id = null();
|
||||||
|
let mut num_configs = 0;
|
||||||
|
let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
|
||||||
|
ensure!(res == egl::TRUE, "error choosing EGL config");
|
||||||
|
ensure!(num_configs != 0, "no EGL config");
|
||||||
|
|
||||||
|
let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
|
||||||
|
.context("error creating EGL context")?;
|
||||||
|
let capabilities = GlesRenderer::supported_capabilities(&egl_context)
|
||||||
|
.context("error getting supported renderer capabilities")?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| *c != Capability::ColorTransformations);
|
||||||
|
|
||||||
|
let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities)
|
||||||
|
.context("error creating GlesRenderer")?;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
|
||||||
|
@extends gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmithayView {
|
||||||
|
pub fn new<T: TestCase + 'static>(
|
||||||
|
make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let obj: Self = glib::Object::builder().build();
|
||||||
|
|
||||||
|
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
|
||||||
|
let make_test_case = Box::new(make) as _;
|
||||||
|
let _ = obj.imp().make_test_case.set(make_test_case);
|
||||||
|
|
||||||
|
obj
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::cmp::{max, min};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use niri::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
|
||||||
|
use niri::render_helpers::renderer::NiriRenderer;
|
||||||
|
use niri::render_helpers::RenderTarget;
|
||||||
|
use niri::window::ResolvedWindowRules;
|
||||||
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use smithay::backend::renderer::element::{Id, Kind};
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
use smithay::utils::{Logical, Point, Scale, Size, Transform};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestWindowInner {
|
||||||
|
size: Size<i32, Logical>,
|
||||||
|
requested_size: Option<Size<i32, Logical>>,
|
||||||
|
min_size: Size<i32, Logical>,
|
||||||
|
max_size: Size<i32, Logical>,
|
||||||
|
buffer: SolidColorBuffer,
|
||||||
|
pending_fullscreen: bool,
|
||||||
|
csd_shadow_width: i32,
|
||||||
|
csd_shadow_buffer: SolidColorBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TestWindow {
|
||||||
|
id: usize,
|
||||||
|
inner: Rc<RefCell<TestWindowInner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestWindow {
|
||||||
|
pub fn freeform(id: usize) -> Self {
|
||||||
|
let size = Size::from((100, 200));
|
||||||
|
let min_size = Size::from((0, 0));
|
||||||
|
let max_size = Size::from((0, 0));
|
||||||
|
let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
inner: Rc::new(RefCell::new(TestWindowInner {
|
||||||
|
size,
|
||||||
|
requested_size: None,
|
||||||
|
min_size,
|
||||||
|
max_size,
|
||||||
|
buffer,
|
||||||
|
pending_fullscreen: false,
|
||||||
|
csd_shadow_width: 0,
|
||||||
|
csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size(id: usize) -> Self {
|
||||||
|
let rv = Self::freeform(id);
|
||||||
|
rv.set_min_size((200, 400).into());
|
||||||
|
rv.set_max_size((200, 400).into());
|
||||||
|
rv.set_color([0.88, 0.11, 0.14, 1.]);
|
||||||
|
rv.communicate();
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_min_size(&self, size: Size<i32, Logical>) {
|
||||||
|
self.inner.borrow_mut().min_size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_size(&self, size: Size<i32, Logical>) {
|
||||||
|
self.inner.borrow_mut().max_size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_color(&self, color: [f32; 4]) {
|
||||||
|
self.inner.borrow_mut().buffer.set_color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_csd_shadow_width(&self, width: i32) {
|
||||||
|
self.inner.borrow_mut().csd_shadow_width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn communicate(&self) -> bool {
|
||||||
|
let mut rv = false;
|
||||||
|
let mut inner = self.inner.borrow_mut();
|
||||||
|
|
||||||
|
let mut new_size = inner.size;
|
||||||
|
|
||||||
|
if let Some(size) = inner.requested_size.take() {
|
||||||
|
assert!(size.w >= 0);
|
||||||
|
assert!(size.h >= 0);
|
||||||
|
|
||||||
|
if size.w != 0 {
|
||||||
|
new_size.w = size.w;
|
||||||
|
}
|
||||||
|
if size.h != 0 {
|
||||||
|
new_size.h = size.h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner.max_size.w > 0 {
|
||||||
|
new_size.w = min(new_size.w, inner.max_size.w);
|
||||||
|
}
|
||||||
|
if inner.max_size.h > 0 {
|
||||||
|
new_size.h = min(new_size.h, inner.max_size.h);
|
||||||
|
}
|
||||||
|
if inner.min_size.w > 0 {
|
||||||
|
new_size.w = max(new_size.w, inner.min_size.w);
|
||||||
|
}
|
||||||
|
if inner.min_size.h > 0 {
|
||||||
|
new_size.h = max(new_size.h, inner.min_size.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner.size != new_size {
|
||||||
|
inner.size = new_size;
|
||||||
|
inner.buffer.resize(new_size);
|
||||||
|
rv = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut csd_shadow_size = new_size;
|
||||||
|
csd_shadow_size.w += inner.csd_shadow_width * 2;
|
||||||
|
csd_shadow_size.h += inner.csd_shadow_width * 2;
|
||||||
|
inner.csd_shadow_buffer.resize(csd_shadow_size);
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutElement for TestWindow {
|
||||||
|
type Id = usize;
|
||||||
|
|
||||||
|
fn id(&self) -> &Self::Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<i32, Logical> {
|
||||||
|
self.inner.borrow().size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buf_loc(&self) -> Point<i32, Logical> {
|
||||||
|
(0, 0).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
_renderer: &mut R,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
alpha: f32,
|
||||||
|
_target: RenderTarget,
|
||||||
|
) -> Vec<LayoutElementRenderElement<R>> {
|
||||||
|
let inner = self.inner.borrow();
|
||||||
|
|
||||||
|
vec![
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
&inner.buffer,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
&inner.csd_shadow_buffer,
|
||||||
|
(location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width)))
|
||||||
|
.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
|
||||||
|
self.inner.borrow_mut().requested_size = Some(size);
|
||||||
|
self.inner.borrow_mut().pending_fullscreen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
|
||||||
|
self.inner.borrow_mut().pending_fullscreen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_size(&self) -> Size<i32, Logical> {
|
||||||
|
self.inner.borrow().min_size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_size(&self) -> Size<i32, Logical> {
|
||||||
|
self.inner.borrow().max_size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {}
|
||||||
|
|
||||||
|
fn has_ssd(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_enter(&self, _output: &Output) {}
|
||||||
|
|
||||||
|
fn output_leave(&self, _output: &Output) {}
|
||||||
|
|
||||||
|
fn set_offscreen_element_id(&self, _id: Option<Id>) {}
|
||||||
|
|
||||||
|
fn set_activated(&mut self, _active: bool) {}
|
||||||
|
|
||||||
|
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||||
|
|
||||||
|
fn send_pending_configure(&mut self) {}
|
||||||
|
|
||||||
|
fn is_fullscreen(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pending_fullscreen(&self) -> bool {
|
||||||
|
self.inner.borrow().pending_fullscreen
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&self) {}
|
||||||
|
|
||||||
|
fn rules(&self) -> &ResolvedWindowRules {
|
||||||
|
static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty();
|
||||||
|
&EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_unmap_snapshot(&self) -> Option<LayoutElementRenderSnapshot> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
+194
-108
@@ -1,6 +1,11 @@
|
|||||||
// This config is in the KDL format: https://kdl.dev
|
// This config is in the KDL format: https://kdl.dev
|
||||||
// "/-" comments out the following node.
|
// "/-" comments out the following node.
|
||||||
|
// Check the wiki for a full description of the configuration:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview
|
||||||
|
|
||||||
|
// Input device configuration.
|
||||||
|
// Find the full list of options on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
|
||||||
input {
|
input {
|
||||||
keyboard {
|
keyboard {
|
||||||
xkb {
|
xkb {
|
||||||
@@ -11,16 +16,6 @@ input {
|
|||||||
// layout "us,ru"
|
// layout "us,ru"
|
||||||
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can set the keyboard repeat parameters. The defaults match wlroots and sway.
|
|
||||||
// Delay is in milliseconds before the repeat starts. Rate is in characters per second.
|
|
||||||
// repeat-delay 600
|
|
||||||
// repeat-rate 25
|
|
||||||
|
|
||||||
// Niri can remember the keyboard layout globally (the default) or per-window.
|
|
||||||
// - "global" - layout change is global for all windows.
|
|
||||||
// - "window" - layout is tracked for each window individually.
|
|
||||||
// track-layout "global"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next sections include libinput settings.
|
// Next sections include libinput settings.
|
||||||
@@ -28,10 +23,10 @@ input {
|
|||||||
touchpad {
|
touchpad {
|
||||||
tap
|
tap
|
||||||
// dwt
|
// dwt
|
||||||
|
// dwtp
|
||||||
natural-scroll
|
natural-scroll
|
||||||
// accel-speed 0.2
|
// accel-speed 0.2
|
||||||
// accel-profile "flat"
|
// accel-profile "flat"
|
||||||
// tap-button-map "left-middle-right"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse {
|
mouse {
|
||||||
@@ -40,86 +35,73 @@ input {
|
|||||||
// accel-profile "flat"
|
// accel-profile "flat"
|
||||||
}
|
}
|
||||||
|
|
||||||
tablet {
|
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||||
// Set the name of the output (see below) which the tablet will map to.
|
// warp-mouse-to-focus
|
||||||
// If this is unset or the output doesn't exist, the tablet maps to one of the
|
|
||||||
// existing outputs.
|
|
||||||
map-to-output "eDP-1"
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, niri will take over the power button to make it sleep
|
// Focus windows and outputs automatically when moving the mouse into them.
|
||||||
// instead of power off.
|
// focus-follows-mouse
|
||||||
// Uncomment this if you would like to configure the power button elsewhere
|
|
||||||
// (i.e. logind.conf).
|
|
||||||
// disable-power-key-handling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can configure outputs by their name, which you can find
|
// You can configure outputs by their name, which you can find
|
||||||
// by running `niri msg outputs` while inside a niri instance.
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
// The built-in laptop monitor is usually called "eDP-1".
|
// The built-in laptop monitor is usually called "eDP-1".
|
||||||
// Remember to uncommend the node by removing "/-"!
|
// Find more information on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
|
||||||
|
// Remember to uncomment the node by removing "/-"!
|
||||||
/-output "eDP-1" {
|
/-output "eDP-1" {
|
||||||
// Uncomment this line to disable this output.
|
// Uncomment this line to disable this output.
|
||||||
// off
|
// off
|
||||||
|
|
||||||
// Scale is a floating-point number, but at the moment only integer values work.
|
|
||||||
scale 2.0
|
|
||||||
|
|
||||||
// Resolution and, optionally, refresh rate of the output.
|
// Resolution and, optionally, refresh rate of the output.
|
||||||
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
|
// The format is "<width>x<height>" or "<width>x<height>@<refresh rate>".
|
||||||
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
// If the refresh rate is omitted, niri will pick the highest refresh rate
|
||||||
// for the resolution.
|
// for the resolution.
|
||||||
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
|
// If the mode is omitted altogether or is invalid, niri will pick one automatically.
|
||||||
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
// Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
||||||
mode "1920x1080@144"
|
mode "1920x1080@120.030"
|
||||||
|
|
||||||
|
// Scale is a floating-point number, but at the moment only integer values work.
|
||||||
|
scale 2.0
|
||||||
|
|
||||||
|
// Transform allows to rotate the output counter-clockwise, valid values are:
|
||||||
|
// normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270.
|
||||||
|
transform "normal"
|
||||||
|
|
||||||
// Position of the output in the global coordinate space.
|
// Position of the output in the global coordinate space.
|
||||||
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
|
// This affects directional monitor actions like "focus-monitor-left", and cursor movement.
|
||||||
// The cursor can only move between directly adjacent outputs.
|
// The cursor can only move between directly adjacent outputs.
|
||||||
// Output scale has to be taken into account for positioning:
|
// Output scale and rotation has to be taken into account for positioning:
|
||||||
// outputs are sized in logical, or scaled, pixels.
|
// outputs are sized in logical, or scaled, pixels.
|
||||||
// For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080,
|
// For example, a 3840×2160 output with scale 2.0 will have a logical size of 1920×1080,
|
||||||
// so to put another output directly adjacent to it on the right, set its x to 1920.
|
// so to put another output directly adjacent to it on the right, set its x to 1920.
|
||||||
// It the position is unset or results in an overlap, the output is instead placed
|
// If the position is unset or results in an overlap, the output is instead placed
|
||||||
// automatically.
|
// automatically.
|
||||||
position x=1280 y=0
|
position x=1280 y=0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings that influence how windows are positioned and sized.
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||||
layout {
|
layout {
|
||||||
// You can change how the focus ring looks.
|
// Set gaps around windows in logical pixels.
|
||||||
focus-ring {
|
gaps 16
|
||||||
// Uncomment this line to disable the focus ring.
|
|
||||||
// off
|
|
||||||
|
|
||||||
// How many logical pixels the ring extends out from the windows.
|
// When to center a column when changing focus, options are:
|
||||||
width 4
|
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||||
|
// or right edge of the screen.
|
||||||
// Color of the ring on the active monitor: red, green, blue, alpha.
|
// - "always", the focused column will always be centered.
|
||||||
active-color 127 200 255 255
|
// - "on-overflow", focusing a column will center it if it doesn't fit
|
||||||
|
// together with the previously focused column.
|
||||||
// Color of the ring on inactive monitors: red, green, blue, alpha.
|
center-focused-column "never"
|
||||||
inactive-color 80 80 80 255
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can also add a border. It's similar to the focus ring, but always visible.
|
|
||||||
border {
|
|
||||||
// The settings are the same as for the focus ring.
|
|
||||||
// If you enable the border, you probably want to disable the focus ring.
|
|
||||||
off
|
|
||||||
|
|
||||||
width 4
|
|
||||||
active-color 255 200 127 255
|
|
||||||
inactive-color 80 80 80 255
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||||
preset-column-widths {
|
preset-column-widths {
|
||||||
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
||||||
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
|
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
|
||||||
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
||||||
proportion 0.333
|
proportion 0.33333
|
||||||
proportion 0.5
|
proportion 0.5
|
||||||
proportion 0.667
|
proportion 0.66667
|
||||||
|
|
||||||
// Fixed sets the width in logical pixels exactly.
|
// Fixed sets the width in logical pixels exactly.
|
||||||
// fixed 1920
|
// fixed 1920
|
||||||
@@ -130,8 +112,64 @@ layout {
|
|||||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||||
// default-column-width {}
|
// default-column-width {}
|
||||||
|
|
||||||
// Set gaps around windows in logical pixels.
|
// By default focus ring and border are rendered as a solid background rectangle
|
||||||
gaps 16
|
// behind windows. That is, they will show up through semitransparent windows.
|
||||||
|
// This is because windows using client-side decorations can have an arbitrary shape.
|
||||||
|
//
|
||||||
|
// If you don't like that, you should uncomment `prefer-no-csd` below.
|
||||||
|
// Niri will draw focus ring and border *around* windows that agree to omit their
|
||||||
|
// client-side decorations.
|
||||||
|
//
|
||||||
|
// Alternatively, you can override it with a window rule called
|
||||||
|
// `draw-border-with-background`.
|
||||||
|
|
||||||
|
// You can change how the focus ring looks.
|
||||||
|
focus-ring {
|
||||||
|
// Uncomment this line to disable the focus ring.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// How many logical pixels the ring extends out from the windows.
|
||||||
|
width 4
|
||||||
|
|
||||||
|
// Colors can be set in a variety of ways:
|
||||||
|
// - CSS named colors: "red"
|
||||||
|
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
|
||||||
|
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
|
||||||
|
|
||||||
|
// Color of the ring on the active monitor.
|
||||||
|
active-color "#7fc8ff"
|
||||||
|
|
||||||
|
// Color of the ring on inactive monitors.
|
||||||
|
inactive-color "#505050"
|
||||||
|
|
||||||
|
// You can also use gradients. They take precedence over solid colors.
|
||||||
|
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
|
||||||
|
// The angle is the same as in linear-gradient, and is optional,
|
||||||
|
// defaulting to 180 (top-to-bottom gradient).
|
||||||
|
// You can use any CSS linear-gradient tool on the web to set these up.
|
||||||
|
//
|
||||||
|
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||||
|
|
||||||
|
// You can also color the gradient relative to the entire view
|
||||||
|
// of the workspace, rather than relative to just the window itself.
|
||||||
|
// To do that, set relative-to="workspace-view".
|
||||||
|
//
|
||||||
|
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can also add a border. It's similar to the focus ring, but always visible.
|
||||||
|
border {
|
||||||
|
// The settings are the same as for the focus ring.
|
||||||
|
// If you enable the border, you probably want to disable the focus ring.
|
||||||
|
off
|
||||||
|
|
||||||
|
width 4
|
||||||
|
active-color "#ffc87f"
|
||||||
|
inactive-color "#505050"
|
||||||
|
|
||||||
|
// active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view"
|
||||||
|
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||||
|
}
|
||||||
|
|
||||||
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
// 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.
|
// You can think of them as a kind of outer gaps. They are set in logical pixels.
|
||||||
@@ -144,14 +182,6 @@ layout {
|
|||||||
// top 64
|
// top 64
|
||||||
// bottom 64
|
// bottom 64
|
||||||
}
|
}
|
||||||
|
|
||||||
// When to center a column when changing focus, options are:
|
|
||||||
// - "never", default behavior, focusing an off-screen column will keep at the left
|
|
||||||
// or right edge of the screen.
|
|
||||||
// - "on-overflow", focusing a column will center it if it doesn't fit
|
|
||||||
// together with the previously focused column.
|
|
||||||
// - "always", the focused column will always be centered.
|
|
||||||
center-focused-column "never"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add lines like this to spawn processes at startup.
|
// Add lines like this to spawn processes at startup.
|
||||||
@@ -159,13 +189,6 @@ layout {
|
|||||||
// which may be more convenient to use.
|
// which may be more convenient to use.
|
||||||
// spawn-at-startup "alacritty" "-e" "fish"
|
// spawn-at-startup "alacritty" "-e" "fish"
|
||||||
|
|
||||||
cursor {
|
|
||||||
// Change the theme and size of the cursor as well as set the
|
|
||||||
// `XCURSOR_THEME` and `XCURSOR_SIZE` env variables.
|
|
||||||
// xcursor-theme "default"
|
|
||||||
// xcursor-size 24
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncomment this line to ask the clients to omit their client-side decorations if possible.
|
// 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.
|
// If the client will specifically ask for CSD, the request will be honored.
|
||||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||||
@@ -179,10 +202,41 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
|||||||
// You can also set this to null to disable saving screenshots to disk.
|
// You can also set this to null to disable saving screenshots to disk.
|
||||||
// screenshot-path null
|
// screenshot-path null
|
||||||
|
|
||||||
// Settings for the "Important Hotkeys" overlay.
|
// Animation settings.
|
||||||
hotkey-overlay {
|
// The wiki explains how to configure individual animations:
|
||||||
// Uncomment this line if you don't want to see the hotkey help at niri startup.
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Animations
|
||||||
// skip-at-startup
|
animations {
|
||||||
|
// Uncomment to turn off all animations.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// Slow down all animations by this factor. Values below 1 speed them up instead.
|
||||||
|
// slowdown 3.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window rules let you adjust behavior for individual windows.
|
||||||
|
// Find more information on the wiki:
|
||||||
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
|
||||||
|
|
||||||
|
// Work around WezTerm's initial configure bug
|
||||||
|
// by setting an empty default-column-width.
|
||||||
|
window-rule {
|
||||||
|
// This regular expression is intentionally made as specific as possible,
|
||||||
|
// since this is the default config, and we want no false positives.
|
||||||
|
// You can get away with just app-id="wezterm" if you want.
|
||||||
|
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||||
|
default-column-width {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: block out two password managers from screen capture.
|
||||||
|
// (This example rule is commented out with a "/-" in front.)
|
||||||
|
/-window-rule {
|
||||||
|
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||||
|
match app-id=r#"^org\.gnome\.World\.Secrets$"#
|
||||||
|
|
||||||
|
block-out-from "screen-capture"
|
||||||
|
|
||||||
|
// Use this instead if you want them visible on third-party screenshot tools.
|
||||||
|
// block-out-from "screencast"
|
||||||
}
|
}
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
@@ -192,6 +246,9 @@ binds {
|
|||||||
//
|
//
|
||||||
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
|
||||||
// when running as a winit window.
|
// when running as a winit window.
|
||||||
|
//
|
||||||
|
// Most actions that you can bind here can also be invoked programmatically with
|
||||||
|
// `niri msg action do-something`.
|
||||||
|
|
||||||
// Mod-Shift-/, which is usually the same as Mod-?,
|
// Mod-Shift-/, which is usually the same as Mod-?,
|
||||||
// shows a list of important hotkeys.
|
// shows a list of important hotkeys.
|
||||||
@@ -200,14 +257,17 @@ binds {
|
|||||||
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
// Suggested binds for running programs: terminal, app launcher, screen locker.
|
||||||
Mod+T { spawn "alacritty"; }
|
Mod+T { spawn "alacritty"; }
|
||||||
Mod+D { spawn "fuzzel"; }
|
Mod+D { spawn "fuzzel"; }
|
||||||
Mod+Alt+L { spawn "swaylock"; }
|
Super+Alt+L { spawn "swaylock"; }
|
||||||
|
|
||||||
// You can also use a shell:
|
// You can also use a shell:
|
||||||
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
// Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; }
|
||||||
|
|
||||||
// Example volume keys mappings for PipeWire & WirePlumber.
|
// Example volume keys mappings for PipeWire & WirePlumber.
|
||||||
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
// The allow-when-locked=true property makes them work even when the session is locked.
|
||||||
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
|
||||||
|
XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }
|
||||||
|
|
||||||
Mod+Q { close-window; }
|
Mod+Q { close-window; }
|
||||||
|
|
||||||
@@ -263,6 +323,10 @@ binds {
|
|||||||
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
|
// And you can also move a whole workspace to another monitor:
|
||||||
|
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
|
||||||
|
// ...
|
||||||
|
|
||||||
Mod+Page_Down { focus-workspace-down; }
|
Mod+Page_Down { focus-workspace-down; }
|
||||||
Mod+Page_Up { focus-workspace-up; }
|
Mod+Page_Up { focus-workspace-up; }
|
||||||
Mod+U { focus-workspace-down; }
|
Mod+U { focus-workspace-down; }
|
||||||
@@ -281,6 +345,46 @@ binds {
|
|||||||
Mod+Shift+U { move-workspace-down; }
|
Mod+Shift+U { move-workspace-down; }
|
||||||
Mod+Shift+I { move-workspace-up; }
|
Mod+Shift+I { move-workspace-up; }
|
||||||
|
|
||||||
|
// You can bind mouse wheel scroll ticks using the following syntax.
|
||||||
|
// These binds will change direction based on the natural-scroll setting.
|
||||||
|
//
|
||||||
|
// To avoid scrolling through workspaces really fast, you can use
|
||||||
|
// the cooldown-ms property. The bind will be rate-limited to this value.
|
||||||
|
// You can set a cooldown on any bind, but it's most useful for the wheel.
|
||||||
|
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
Mod+WheelScrollRight { focus-column-right; }
|
||||||
|
Mod+WheelScrollLeft { focus-column-left; }
|
||||||
|
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||||
|
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||||
|
|
||||||
|
// Usually scrolling up and down with Shift in applications results in
|
||||||
|
// horizontal scrolling; these binds replicate that.
|
||||||
|
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||||
|
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||||
|
|
||||||
|
// Similarly, you can bind touchpad scroll "ticks".
|
||||||
|
// Touchpad scrolling is continuous, so for these binds it is split into
|
||||||
|
// discrete intervals.
|
||||||
|
// These binds are also affected by touchpad's natural-scroll, so these
|
||||||
|
// example binds are "inverted", since we have natural-scroll enabled for
|
||||||
|
// touchpads by default.
|
||||||
|
// Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
|
||||||
|
// Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
|
||||||
|
|
||||||
|
// You can refer to workspaces by index. However, keep in mind that
|
||||||
|
// niri is a dynamic workspace system, so these commands are kind of
|
||||||
|
// "best effort". Trying to refer to a workspace index bigger than
|
||||||
|
// the current workspace count will instead refer to the bottommost
|
||||||
|
// (empty) workspace.
|
||||||
|
//
|
||||||
|
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
|
||||||
|
// will all refer to the 3rd workspace.
|
||||||
Mod+1 { focus-workspace 1; }
|
Mod+1 { focus-workspace 1; }
|
||||||
Mod+2 { focus-workspace 2; }
|
Mod+2 { focus-workspace 2; }
|
||||||
Mod+3 { focus-workspace 3; }
|
Mod+3 { focus-workspace 3; }
|
||||||
@@ -303,9 +407,16 @@ binds {
|
|||||||
// Alternatively, there are commands to move just a single window:
|
// Alternatively, there are commands to move just a single window:
|
||||||
// Mod+Ctrl+1 { move-window-to-workspace 1; }
|
// Mod+Ctrl+1 { move-window-to-workspace 1; }
|
||||||
|
|
||||||
|
// Switches focus between the current and the previous workspace.
|
||||||
|
// Mod+Tab { focus-workspace-previous; }
|
||||||
|
|
||||||
Mod+Comma { consume-window-into-column; }
|
Mod+Comma { consume-window-into-column; }
|
||||||
Mod+Period { expel-window-from-column; }
|
Mod+Period { expel-window-from-column; }
|
||||||
|
|
||||||
|
// There are also commands that consume or expel a single window to the side.
|
||||||
|
// Mod+BracketLeft { consume-or-expel-window-left; }
|
||||||
|
// Mod+BracketRight { consume-or-expel-window-right; }
|
||||||
|
|
||||||
Mod+R { switch-preset-column-width; }
|
Mod+R { switch-preset-column-width; }
|
||||||
Mod+F { maximize-column; }
|
Mod+F { maximize-column; }
|
||||||
Mod+Shift+F { fullscreen-window; }
|
Mod+Shift+F { fullscreen-window; }
|
||||||
@@ -338,35 +449,10 @@ binds {
|
|||||||
Ctrl+Print { screenshot-screen; }
|
Ctrl+Print { screenshot-screen; }
|
||||||
Alt+Print { screenshot-window; }
|
Alt+Print { screenshot-window; }
|
||||||
|
|
||||||
|
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||||
Mod+Shift+E { quit; }
|
Mod+Shift+E { quit; }
|
||||||
|
|
||||||
|
// Powers off the monitors. To turn them back on, do any input like
|
||||||
|
// moving the mouse or pressing any other key.
|
||||||
Mod+Shift+P { power-off-monitors; }
|
Mod+Shift+P { power-off-monitors; }
|
||||||
|
|
||||||
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings for debugging. Not meant for normal use.
|
|
||||||
// These can change or stop working at any point with little notice.
|
|
||||||
debug {
|
|
||||||
// Make niri take over its DBus services even if it's not running as a session.
|
|
||||||
// Useful for testing screen recording changes without having to relogin.
|
|
||||||
// The main niri instance will *not* currently take back the services; so you will
|
|
||||||
// need to relogin in the end.
|
|
||||||
// dbus-interfaces-in-non-session-instances
|
|
||||||
|
|
||||||
// Wait until every frame is done rendering before handing it over to DRM.
|
|
||||||
// wait-for-frame-completion-before-queueing
|
|
||||||
|
|
||||||
// Enable direct scanout into overlay planes.
|
|
||||||
// May cause frame drops during some animations on some hardware.
|
|
||||||
// enable-overlay-planes
|
|
||||||
|
|
||||||
// Disable the use of the cursor plane.
|
|
||||||
// The cursor will be rendered together with the rest of the frame.
|
|
||||||
// disable-cursor-plane
|
|
||||||
|
|
||||||
// Slow down animations by this factor.
|
|
||||||
// animation-slowdown 3.0
|
|
||||||
|
|
||||||
// Override the DRM device that niri will use for all rendering.
|
|
||||||
// render-drm-device "/dev/dri/renderD129"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ fi
|
|||||||
# Reset failed state of all user units.
|
# Reset failed state of all user units.
|
||||||
systemctl --user reset-failed
|
systemctl --user reset-failed
|
||||||
|
|
||||||
# Set the current desktop for xdg-desktop-portal.
|
|
||||||
export XDG_CURRENT_DESKTOP=niri
|
|
||||||
|
|
||||||
# Ensure the session type is set to Wayland for xdg-autostart apps.
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
|
|
||||||
# Import the login manager environment.
|
# Import the login manager environment.
|
||||||
systemctl --user import-environment
|
systemctl --user import-environment
|
||||||
|
|
||||||
@@ -44,4 +38,4 @@ systemctl --user --wait start niri.service
|
|||||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||||
|
|
||||||
# Unset environment that we've set.
|
# Unset environment that we've set.
|
||||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP
|
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ Wants=xdg-desktop-autostart.target
|
|||||||
Before=xdg-desktop-autostart.target
|
Before=xdg-desktop-autostart.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
Slice=session.slice
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=/usr/bin/niri
|
ExecStart=/usr/bin/niri --session
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use keyframe::functions::EaseOutCubic;
|
|
||||||
use keyframe::EasingFunction;
|
|
||||||
use portable_atomic::{AtomicF64, Ordering};
|
|
||||||
|
|
||||||
use crate::utils::get_monotonic_time;
|
|
||||||
|
|
||||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Animation {
|
|
||||||
from: f64,
|
|
||||||
to: f64,
|
|
||||||
duration: Duration,
|
|
||||||
start_time: Duration,
|
|
||||||
current_time: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Animation {
|
|
||||||
pub fn new(from: f64, to: f64, over: Duration) -> Self {
|
|
||||||
// FIXME: ideally we shouldn't use current time here because animations started within the
|
|
||||||
// same frame cycle should have the same start time to be synchronized.
|
|
||||||
let now = get_monotonic_time();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)),
|
|
||||||
start_time: now,
|
|
||||||
current_time: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_time(&mut self, time: Duration) {
|
|
||||||
self.current_time = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
|
||||||
self.current_time >= self.start_time + self.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn value(&self) -> f64 {
|
|
||||||
let passed = (self.current_time - self.start_time).as_secs_f64();
|
|
||||||
let total = self.duration.as_secs_f64();
|
|
||||||
let x = (passed / total).clamp(0., 1.);
|
|
||||||
EaseOutCubic.y(x) * (self.to - self.from) + self.from
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to(&self) -> f64 {
|
|
||||||
self.to
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn from(&self) -> f64 {
|
|
||||||
self.from
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use keyframe::functions::{EaseOutCubic, EaseOutQuad};
|
||||||
|
use keyframe::EasingFunction;
|
||||||
|
use portable_atomic::{AtomicF64, Ordering};
|
||||||
|
|
||||||
|
use crate::utils::get_monotonic_time;
|
||||||
|
|
||||||
|
mod spring;
|
||||||
|
pub use spring::{Spring, SpringParams};
|
||||||
|
|
||||||
|
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Animation {
|
||||||
|
from: f64,
|
||||||
|
to: f64,
|
||||||
|
initial_velocity: f64,
|
||||||
|
is_off: bool,
|
||||||
|
duration: Duration,
|
||||||
|
/// Time until the animation first reaches `to`.
|
||||||
|
///
|
||||||
|
/// Best effort; not always exactly precise.
|
||||||
|
clamped_duration: Duration,
|
||||||
|
start_time: Duration,
|
||||||
|
current_time: Duration,
|
||||||
|
kind: Kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Kind {
|
||||||
|
Easing {
|
||||||
|
curve: Curve,
|
||||||
|
},
|
||||||
|
Spring(Spring),
|
||||||
|
Deceleration {
|
||||||
|
initial_velocity: f64,
|
||||||
|
deceleration_rate: f64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Curve {
|
||||||
|
EaseOutQuad,
|
||||||
|
EaseOutCubic,
|
||||||
|
EaseOutExpo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation {
|
||||||
|
pub fn new(from: f64, to: f64, initial_velocity: f64, config: niri_config::Animation) -> Self {
|
||||||
|
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||||
|
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
let mut rv = Self::ease(from, to, initial_velocity, 0, Curve::EaseOutCubic);
|
||||||
|
if config.off {
|
||||||
|
rv.is_off = true;
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
rv.replace_config(config);
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_config(&mut self, config: niri_config::Animation) {
|
||||||
|
self.is_off = config.off;
|
||||||
|
if config.off {
|
||||||
|
self.duration = Duration::ZERO;
|
||||||
|
self.clamped_duration = Duration::ZERO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_time = self.start_time;
|
||||||
|
let current_time = self.current_time;
|
||||||
|
|
||||||
|
match config.kind {
|
||||||
|
niri_config::AnimationKind::Spring(p) => {
|
||||||
|
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
|
||||||
|
|
||||||
|
let spring = Spring {
|
||||||
|
from: self.from,
|
||||||
|
to: self.to,
|
||||||
|
initial_velocity: self.initial_velocity,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
*self = Self::spring(spring);
|
||||||
|
}
|
||||||
|
niri_config::AnimationKind::Easing(p) => {
|
||||||
|
*self = Self::ease(
|
||||||
|
self.from,
|
||||||
|
self.to,
|
||||||
|
self.initial_velocity,
|
||||||
|
u64::from(p.duration_ms),
|
||||||
|
Curve::from(p.curve),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.start_time = start_time;
|
||||||
|
self.current_time = current_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restarts the animation using the previous config.
|
||||||
|
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||||
|
if self.is_off {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||||
|
let initial_velocity = initial_velocity * ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
match self.kind {
|
||||||
|
Kind::Easing { curve } => Self::ease(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
initial_velocity,
|
||||||
|
self.duration.as_millis() as u64,
|
||||||
|
curve,
|
||||||
|
),
|
||||||
|
Kind::Spring(spring) => {
|
||||||
|
let spring = Spring {
|
||||||
|
from: self.from,
|
||||||
|
to: self.to,
|
||||||
|
initial_velocity: self.initial_velocity,
|
||||||
|
params: spring.params,
|
||||||
|
};
|
||||||
|
Self::spring(spring)
|
||||||
|
}
|
||||||
|
Kind::Deceleration {
|
||||||
|
initial_velocity,
|
||||||
|
deceleration_rate,
|
||||||
|
} => {
|
||||||
|
let threshold = 0.001; // FIXME
|
||||||
|
Self::decelerate(from, initial_velocity, deceleration_rate, threshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ease(from: f64, to: f64, initial_velocity: f64, duration_ms: u64, curve: Curve) -> Self {
|
||||||
|
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||||
|
// same frame cycle should have the same start time to be synchronized.
|
||||||
|
let now = get_monotonic_time();
|
||||||
|
|
||||||
|
let duration = Duration::from_millis(duration_ms);
|
||||||
|
let kind = Kind::Easing { curve };
|
||||||
|
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
initial_velocity,
|
||||||
|
is_off: false,
|
||||||
|
duration,
|
||||||
|
// Our current curves never overshoot.
|
||||||
|
clamped_duration: duration,
|
||||||
|
start_time: now,
|
||||||
|
current_time: now,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spring(spring: Spring) -> Self {
|
||||||
|
let _span = tracy_client::span!("Animation::spring");
|
||||||
|
|
||||||
|
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||||
|
// same frame cycle should have the same start time to be synchronized.
|
||||||
|
let now = get_monotonic_time();
|
||||||
|
|
||||||
|
let duration = spring.duration();
|
||||||
|
let clamped_duration = spring.clamped_duration().unwrap_or(duration);
|
||||||
|
let kind = Kind::Spring(spring);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
from: spring.from,
|
||||||
|
to: spring.to,
|
||||||
|
initial_velocity: spring.initial_velocity,
|
||||||
|
is_off: false,
|
||||||
|
duration,
|
||||||
|
clamped_duration,
|
||||||
|
start_time: now,
|
||||||
|
current_time: now,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decelerate(
|
||||||
|
from: f64,
|
||||||
|
initial_velocity: f64,
|
||||||
|
deceleration_rate: f64,
|
||||||
|
threshold: f64,
|
||||||
|
) -> Self {
|
||||||
|
// FIXME: ideally we shouldn't use current time here because animations started within the
|
||||||
|
// same frame cycle should have the same start time to be synchronized.
|
||||||
|
let now = get_monotonic_time();
|
||||||
|
|
||||||
|
let duration_s = if initial_velocity == 0. {
|
||||||
|
0.
|
||||||
|
} else {
|
||||||
|
let coeff = 1000. * deceleration_rate.ln();
|
||||||
|
(-coeff * threshold / initial_velocity.abs()).ln() / coeff
|
||||||
|
};
|
||||||
|
let duration = Duration::from_secs_f64(duration_s);
|
||||||
|
|
||||||
|
let to = from - initial_velocity / (1000. * deceleration_rate.ln());
|
||||||
|
|
||||||
|
let kind = Kind::Deceleration {
|
||||||
|
initial_velocity,
|
||||||
|
deceleration_rate,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
initial_velocity,
|
||||||
|
is_off: false,
|
||||||
|
duration,
|
||||||
|
clamped_duration: duration,
|
||||||
|
start_time: now,
|
||||||
|
current_time: now,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_time(&mut self, time: Duration) {
|
||||||
|
if self.duration.is_zero() {
|
||||||
|
self.current_time = time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end_time = self.start_time + self.duration;
|
||||||
|
if end_time <= self.current_time {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let slowdown = ANIMATION_SLOWDOWN.load(Ordering::Relaxed);
|
||||||
|
if slowdown <= f64::EPSILON {
|
||||||
|
// Zero slowdown will cause the animation to end right away.
|
||||||
|
self.current_time = end_time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't change current_time (since the incoming time values are always real-time), so
|
||||||
|
// apply the slowdown by shifting the start time to compensate.
|
||||||
|
if self.current_time <= time {
|
||||||
|
let delta = time - self.current_time;
|
||||||
|
|
||||||
|
let max_delta = end_time - self.current_time;
|
||||||
|
let min_slowdown = delta.as_secs_f64() / max_delta.as_secs_f64();
|
||||||
|
if slowdown <= min_slowdown {
|
||||||
|
// Our slowdown value will cause the animation to end right away.
|
||||||
|
self.current_time = end_time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjusted_delta = delta.div_f64(slowdown);
|
||||||
|
if adjusted_delta >= delta {
|
||||||
|
self.start_time -= adjusted_delta - delta;
|
||||||
|
} else {
|
||||||
|
self.start_time += delta - adjusted_delta;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let delta = self.current_time - time;
|
||||||
|
|
||||||
|
let min_slowdown = delta.as_secs_f64() / self.current_time.as_secs_f64();
|
||||||
|
if slowdown <= min_slowdown {
|
||||||
|
// Current time was about to jump to before the animation had started; let's just
|
||||||
|
// cancel the animation in this case.
|
||||||
|
self.current_time = end_time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjusted_delta = delta.div_f64(slowdown);
|
||||||
|
if adjusted_delta >= delta {
|
||||||
|
self.start_time += adjusted_delta - delta;
|
||||||
|
} else {
|
||||||
|
self.start_time -= delta - adjusted_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_done(&self) -> bool {
|
||||||
|
self.current_time >= self.start_time + self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_clamped_done(&self) -> bool {
|
||||||
|
self.current_time >= self.start_time + self.clamped_duration
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(&self) -> f64 {
|
||||||
|
if self.is_done() {
|
||||||
|
return self.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
let passed = self.current_time - self.start_time;
|
||||||
|
|
||||||
|
match self.kind {
|
||||||
|
Kind::Easing { curve } => {
|
||||||
|
let passed = passed.as_secs_f64();
|
||||||
|
let total = self.duration.as_secs_f64();
|
||||||
|
let x = (passed / total).clamp(0., 1.);
|
||||||
|
curve.y(x) * (self.to - self.from) + self.from
|
||||||
|
}
|
||||||
|
Kind::Spring(spring) => {
|
||||||
|
let value = spring.value_at(passed);
|
||||||
|
|
||||||
|
// Protect against numerical instability.
|
||||||
|
let range = (self.to - self.from) * 10.;
|
||||||
|
let a = self.from - range;
|
||||||
|
let b = self.to + range;
|
||||||
|
if self.from <= self.to {
|
||||||
|
value.clamp(a, b)
|
||||||
|
} else {
|
||||||
|
value.clamp(b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Kind::Deceleration {
|
||||||
|
initial_velocity,
|
||||||
|
deceleration_rate,
|
||||||
|
} => {
|
||||||
|
let passed = passed.as_secs_f64();
|
||||||
|
let coeff = 1000. * deceleration_rate.ln();
|
||||||
|
self.from + (deceleration_rate.powf(1000. * passed) - 1.) / coeff * initial_velocity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a value that stops at the target value after first reaching it.
|
||||||
|
///
|
||||||
|
/// Best effort; not always exactly precise.
|
||||||
|
pub fn clamped_value(&self) -> f64 {
|
||||||
|
if self.is_clamped_done() {
|
||||||
|
return self.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to(&self) -> f64 {
|
||||||
|
self.to
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn from(&self) -> f64 {
|
||||||
|
self.from
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn offset(&mut self, offset: f64) {
|
||||||
|
self.from += offset;
|
||||||
|
self.to += offset;
|
||||||
|
|
||||||
|
if let Kind::Spring(spring) = &mut self.kind {
|
||||||
|
spring.from += offset;
|
||||||
|
spring.to += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Curve {
|
||||||
|
pub fn y(self, x: f64) -> f64 {
|
||||||
|
match self {
|
||||||
|
Curve::EaseOutQuad => EaseOutQuad.y(x),
|
||||||
|
Curve::EaseOutCubic => EaseOutCubic.y(x),
|
||||||
|
Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<niri_config::AnimationCurve> for Curve {
|
||||||
|
fn from(value: niri_config::AnimationCurve) -> Self {
|
||||||
|
match value {
|
||||||
|
niri_config::AnimationCurve::EaseOutQuad => Curve::EaseOutQuad,
|
||||||
|
niri_config::AnimationCurve::EaseOutCubic => Curve::EaseOutCubic,
|
||||||
|
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SpringParams {
|
||||||
|
pub damping: f64,
|
||||||
|
pub mass: f64,
|
||||||
|
pub stiffness: f64,
|
||||||
|
pub epsilon: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Spring {
|
||||||
|
pub from: f64,
|
||||||
|
pub to: f64,
|
||||||
|
pub initial_velocity: f64,
|
||||||
|
pub params: SpringParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpringParams {
|
||||||
|
pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self {
|
||||||
|
let damping_ratio = damping_ratio.max(0.);
|
||||||
|
let stiffness = stiffness.max(0.);
|
||||||
|
let epsilon = epsilon.max(0.);
|
||||||
|
|
||||||
|
let mass = 1.;
|
||||||
|
let critical_damping = 2. * (mass * stiffness).sqrt();
|
||||||
|
let damping = damping_ratio * critical_damping;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
damping,
|
||||||
|
mass,
|
||||||
|
stiffness,
|
||||||
|
epsilon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spring {
|
||||||
|
pub fn value_at(&self, t: Duration) -> f64 {
|
||||||
|
self.oscillate(t.as_secs_f64())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on libadwaita (LGPL-2.1-or-later):
|
||||||
|
// https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c,
|
||||||
|
// which itself is based on (MIT):
|
||||||
|
// https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m
|
||||||
|
/// Computes and returns the duration until the spring is at rest.
|
||||||
|
pub fn duration(&self) -> Duration {
|
||||||
|
const DELTA: f64 = 0.001;
|
||||||
|
|
||||||
|
let beta = self.params.damping / (2. * self.params.mass);
|
||||||
|
|
||||||
|
if beta.abs() <= f64::EPSILON || beta < 0. {
|
||||||
|
return Duration::MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
let omega0 = (self.params.stiffness / self.params.mass).sqrt();
|
||||||
|
|
||||||
|
// As first ansatz for the overdamped solution,
|
||||||
|
// and general estimation for the oscillating ones
|
||||||
|
// we take the value of the envelope when it's < epsilon.
|
||||||
|
let mut x0 = -self.params.epsilon.ln() / beta;
|
||||||
|
|
||||||
|
// f64::EPSILON is too small for this specific comparison, so we use
|
||||||
|
// f32::EPSILON even though it's doubles.
|
||||||
|
if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 {
|
||||||
|
return Duration::from_secs_f64(x0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the overdamped solution decays way slower than the envelope
|
||||||
|
// we need to use the value of the oscillation itself.
|
||||||
|
// Newton's root finding method is a good candidate in this particular case:
|
||||||
|
// https://en.wikipedia.org/wiki/Newton%27s_method
|
||||||
|
let mut y0 = self.oscillate(x0);
|
||||||
|
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
|
||||||
|
|
||||||
|
let mut x1 = (self.to - y0 + m * x0) / m;
|
||||||
|
let mut y1 = self.oscillate(x1);
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
while (self.to - y1).abs() > self.params.epsilon {
|
||||||
|
if i > 1000 {
|
||||||
|
return Duration::ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
x0 = x1;
|
||||||
|
y0 = y1;
|
||||||
|
|
||||||
|
let m = (self.oscillate(x0 + DELTA) - y0) / DELTA;
|
||||||
|
|
||||||
|
x1 = (self.to - y0 + m * x0) / m;
|
||||||
|
y1 = self.oscillate(x1);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration::from_secs_f64(x1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes and returns the duration until the spring reaches its target position.
|
||||||
|
pub fn clamped_duration(&self) -> Option<Duration> {
|
||||||
|
let beta = self.params.damping / (2. * self.params.mass);
|
||||||
|
|
||||||
|
if beta.abs() <= f64::EPSILON || beta < 0. {
|
||||||
|
return Some(Duration::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.to - self.from).abs() <= f64::EPSILON {
|
||||||
|
return Some(Duration::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first frame is not that important and we avoid finding the trivial 0 for in-place
|
||||||
|
// animations.
|
||||||
|
let mut i = 1u16;
|
||||||
|
let mut y = self.oscillate(f64::from(i) / 1000.);
|
||||||
|
|
||||||
|
while (self.to - self.from > f64::EPSILON && self.to - y > self.params.epsilon)
|
||||||
|
|| (self.from - self.to > f64::EPSILON && y - self.to > self.params.epsilon)
|
||||||
|
{
|
||||||
|
if i > 3000 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
y = self.oscillate(f64::from(i) / 1000.);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Duration::from_millis(u64::from(i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the spring position at a given time in seconds.
|
||||||
|
fn oscillate(&self, t: f64) -> f64 {
|
||||||
|
let b = self.params.damping;
|
||||||
|
let m = self.params.mass;
|
||||||
|
let k = self.params.stiffness;
|
||||||
|
let v0 = self.initial_velocity;
|
||||||
|
|
||||||
|
let beta = b / (2. * m);
|
||||||
|
let omega0 = (k / m).sqrt();
|
||||||
|
|
||||||
|
let x0 = self.from - self.to;
|
||||||
|
|
||||||
|
let envelope = (-beta * t).exp();
|
||||||
|
|
||||||
|
// Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
|
||||||
|
// for the differential equation m*ẍ+b*ẋ+kx = 0
|
||||||
|
|
||||||
|
// f64::EPSILON is too small for this specific comparison, so we use
|
||||||
|
// f32::EPSILON even though it's doubles.
|
||||||
|
if (beta - omega0).abs() <= f64::from(f32::EPSILON) {
|
||||||
|
// Critically damped.
|
||||||
|
self.to + envelope * (x0 + (beta * x0 + v0) * t)
|
||||||
|
} else if beta < omega0 {
|
||||||
|
// Underdamped.
|
||||||
|
let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt();
|
||||||
|
|
||||||
|
self.to
|
||||||
|
+ envelope
|
||||||
|
* (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin())
|
||||||
|
} else {
|
||||||
|
// Overdamped.
|
||||||
|
let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt();
|
||||||
|
|
||||||
|
self.to
|
||||||
|
+ envelope
|
||||||
|
* (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-14
@@ -1,6 +1,4 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -10,7 +8,7 @@ use smithay::output::Output;
|
|||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
|
||||||
use crate::input::CompositorMod;
|
use crate::input::CompositorMod;
|
||||||
use crate::Niri;
|
use crate::niri::Niri;
|
||||||
|
|
||||||
pub mod tty;
|
pub mod tty;
|
||||||
pub use tty::Tty;
|
pub use tty::Tty;
|
||||||
@@ -33,6 +31,8 @@ pub enum RenderResult {
|
|||||||
Skipped,
|
Skipped,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
pub fn init(&mut self, niri: &mut Niri) {
|
pub fn init(&mut self, niri: &mut Niri) {
|
||||||
match self {
|
match self {
|
||||||
@@ -98,7 +98,7 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
Backend::Tty(tty) => tty.import_dmabuf(dmabuf),
|
||||||
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
Backend::Winit(winit) => winit.import_dmabuf(dmabuf),
|
||||||
@@ -112,21 +112,13 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
|
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.ipc_outputs(),
|
Backend::Tty(tty) => tty.ipc_outputs(),
|
||||||
Backend::Winit(winit) => winit.ipc_outputs(),
|
Backend::Winit(winit) => winit.ipc_outputs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(feature = "dbus"), allow(unused))]
|
|
||||||
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
|
||||||
match self {
|
|
||||||
Backend::Tty(tty) => tty.enabled_outputs(),
|
|
||||||
Backend::Winit(winit) => winit.enabled_outputs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
pub fn gbm_device(
|
pub fn gbm_device(
|
||||||
&self,
|
&self,
|
||||||
@@ -138,7 +130,7 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_monitors_active(&self, active: bool) {
|
pub fn set_monitors_active(&mut self, active: bool) {
|
||||||
match self {
|
match self {
|
||||||
Backend::Tty(tty) => tty.set_monitors_active(active),
|
Backend::Tty(tty) => tty.set_monitors_active(active),
|
||||||
Backend::Winit(_) => (),
|
Backend::Winit(_) => (),
|
||||||
|
|||||||
+907
-242
File diff suppressed because it is too large
Load Diff
+58
-49
@@ -16,29 +16,30 @@ use smithay::reexports::calloop::LoopHandle;
|
|||||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||||
use smithay::reexports::winit::dpi::LogicalSize;
|
use smithay::reexports::winit::dpi::LogicalSize;
|
||||||
use smithay::reexports::winit::window::WindowBuilder;
|
use smithay::reexports::winit::window::WindowBuilder;
|
||||||
use smithay::utils::Transform;
|
|
||||||
|
|
||||||
use super::RenderResult;
|
use super::{IpcOutputMap, RenderResult};
|
||||||
use crate::niri::{RedrawState, State};
|
use crate::niri::{Niri, RedrawState, State};
|
||||||
use crate::utils::get_monotonic_time;
|
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||||
use crate::Niri;
|
use crate::utils::{get_monotonic_time, logical_output};
|
||||||
|
|
||||||
pub struct Winit {
|
pub struct Winit {
|
||||||
config: Rc<RefCell<Config>>,
|
config: Rc<RefCell<Config>>,
|
||||||
output: Output,
|
output: Output,
|
||||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||||
damage_tracker: OutputDamageTracker,
|
damage_tracker: OutputDamageTracker,
|
||||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Winit {
|
impl Winit {
|
||||||
pub fn new(config: Rc<RefCell<Config>>, event_loop: LoopHandle<State>) -> Self {
|
pub fn new(
|
||||||
|
config: Rc<RefCell<Config>>,
|
||||||
|
event_loop: LoopHandle<State>,
|
||||||
|
) -> Result<Self, winit::Error> {
|
||||||
let builder = WindowBuilder::new()
|
let builder = WindowBuilder::new()
|
||||||
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
.with_inner_size(LogicalSize::new(1280.0, 800.0))
|
||||||
// .with_resizable(false)
|
// .with_resizable(false)
|
||||||
.with_title("niri");
|
.with_title("niri");
|
||||||
let (backend, winit) = winit::init_from_builder(builder).unwrap();
|
let (backend, winit) = winit::init_from_builder(builder)?;
|
||||||
|
|
||||||
let output = Output::new(
|
let output = Output::new(
|
||||||
"winit".to_string(),
|
"winit".to_string(),
|
||||||
@@ -54,11 +55,11 @@ impl Winit {
|
|||||||
size: backend.window_size(),
|
size: backend.window_size(),
|
||||||
refresh: 60_000,
|
refresh: 60_000,
|
||||||
};
|
};
|
||||||
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, None);
|
output.change_current_state(Some(mode), None, None, None);
|
||||||
output.set_preferred(mode);
|
output.set_preferred(mode);
|
||||||
|
|
||||||
let physical_properties = output.physical_properties();
|
let physical_properties = output.physical_properties();
|
||||||
let ipc_outputs = Rc::new(RefCell::new(HashMap::from([(
|
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
|
||||||
"winit".to_owned(),
|
"winit".to_owned(),
|
||||||
niri_ipc::Output {
|
niri_ipc::Output {
|
||||||
name: output.name(),
|
name: output.name(),
|
||||||
@@ -69,16 +70,15 @@ impl Winit {
|
|||||||
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
|
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
|
||||||
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
|
height: backend.window_size().h.clamp(0, u16::MAX as i32) as u16,
|
||||||
refresh_rate: 60_000,
|
refresh_rate: 60_000,
|
||||||
|
is_preferred: true,
|
||||||
}],
|
}],
|
||||||
current_mode: Some(0),
|
current_mode: Some(0),
|
||||||
|
vrr_supported: false,
|
||||||
|
vrr_enabled: false,
|
||||||
|
logical: Some(logical_output(&output)),
|
||||||
},
|
},
|
||||||
)])));
|
)])));
|
||||||
|
|
||||||
let enabled_outputs = Arc::new(Mutex::new(HashMap::from([(
|
|
||||||
"winit".to_owned(),
|
|
||||||
output.clone(),
|
|
||||||
)])));
|
|
||||||
|
|
||||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
let damage_tracker = OutputDamageTracker::from_output(&output);
|
||||||
|
|
||||||
event_loop
|
event_loop
|
||||||
@@ -95,45 +95,47 @@ impl Winit {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut ipc_outputs = winit.ipc_outputs.borrow_mut();
|
{
|
||||||
let mode = &mut ipc_outputs.get_mut("winit").unwrap().modes[0];
|
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
|
||||||
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
|
let output = ipc_outputs.get_mut("winit").unwrap();
|
||||||
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
|
let mode = &mut output.modes[0];
|
||||||
|
mode.width = size.w.clamp(0, u16::MAX as i32) as u16;
|
||||||
|
mode.height = size.h.clamp(0, u16::MAX as i32) as u16;
|
||||||
|
if let Some(logical) = output.logical.as_mut() {
|
||||||
|
logical.width = size.w as u32;
|
||||||
|
logical.height = size.h as u32;
|
||||||
|
}
|
||||||
|
state.niri.ipc_outputs_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
state.niri.output_resized(winit.output.clone());
|
state.niri.output_resized(&winit.output);
|
||||||
}
|
}
|
||||||
WinitEvent::Input(event) => state.process_input_event(event),
|
WinitEvent::Input(event) => state.process_input_event(event),
|
||||||
WinitEvent::Focus(_) => (),
|
WinitEvent::Focus(_) => (),
|
||||||
WinitEvent::Redraw => state
|
WinitEvent::Redraw => state.niri.queue_redraw(&state.backend.winit().output),
|
||||||
.niri
|
WinitEvent::CloseRequested => state.niri.stop_signal.stop(),
|
||||||
.queue_redraw(state.backend.winit().output.clone()),
|
|
||||||
WinitEvent::CloseRequested => {
|
|
||||||
state.niri.stop_signal.stop();
|
|
||||||
state.niri.remove_output(&state.backend.winit().output);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
output,
|
output,
|
||||||
backend,
|
backend,
|
||||||
damage_tracker,
|
damage_tracker,
|
||||||
ipc_outputs,
|
ipc_outputs,
|
||||||
enabled_outputs,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&mut self, niri: &mut Niri) {
|
pub fn init(&mut self, niri: &mut Niri) {
|
||||||
if let Err(err) = self
|
let renderer = self.backend.renderer();
|
||||||
.backend
|
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
||||||
.renderer()
|
|
||||||
.bind_wl_display(&niri.display_handle)
|
|
||||||
{
|
|
||||||
warn!("error binding renderer wl_display: {err}");
|
warn!("error binding renderer wl_display: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
niri.add_output(self.output.clone(), None);
|
resources::init(renderer);
|
||||||
|
shaders::init(renderer);
|
||||||
|
|
||||||
|
niri.add_output(self.output.clone(), None, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seat_name(&self) -> String {
|
pub fn seat_name(&self) -> String {
|
||||||
@@ -151,7 +153,12 @@ impl Winit {
|
|||||||
let _span = tracy_client::span!("Winit::render");
|
let _span = tracy_client::span!("Winit::render");
|
||||||
|
|
||||||
// Render the elements.
|
// Render the elements.
|
||||||
let elements = niri.render::<GlesRenderer>(self.backend.renderer(), output, true);
|
let elements = niri.render::<GlesRenderer>(
|
||||||
|
self.backend.renderer(),
|
||||||
|
output,
|
||||||
|
true,
|
||||||
|
RenderTarget::Output,
|
||||||
|
);
|
||||||
|
|
||||||
// Hand them over to winit.
|
// Hand them over to winit.
|
||||||
self.backend.bind().unwrap();
|
self.backend.bind().unwrap();
|
||||||
@@ -172,10 +179,12 @@ impl Winit {
|
|||||||
.wait_for_frame_completion_before_queueing
|
.wait_for_frame_completion_before_queueing
|
||||||
{
|
{
|
||||||
let _span = tracy_client::span!("wait for completion");
|
let _span = tracy_client::span!("wait for completion");
|
||||||
res.sync.wait();
|
if let Err(err) = res.sync.wait() {
|
||||||
|
warn!("error waiting for frame completion: {err:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.backend.submit(Some(&damage)).unwrap();
|
self.backend.submit(Some(damage)).unwrap();
|
||||||
|
|
||||||
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
|
let mut presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states);
|
||||||
let mode = output.current_mode().unwrap();
|
let mode = output.current_mode().unwrap();
|
||||||
@@ -195,12 +204,16 @@ impl Winit {
|
|||||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||||
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
||||||
RedrawState::Idle => unreachable!(),
|
RedrawState::Idle => unreachable!(),
|
||||||
RedrawState::Queued(_) => (),
|
RedrawState::Queued => (),
|
||||||
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
||||||
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
||||||
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
||||||
|
|
||||||
|
// FIXME: this should wait until a frame callback from the host compositor, but it redraws
|
||||||
|
// right away instead.
|
||||||
if output_state.unfinished_animations_remain {
|
if output_state.unfinished_animations_remain {
|
||||||
self.backend.window().request_redraw();
|
self.backend.window().request_redraw();
|
||||||
}
|
}
|
||||||
@@ -213,21 +226,17 @@ impl Winit {
|
|||||||
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> {
|
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
||||||
match self.backend.renderer().import_dmabuf(dmabuf, None) {
|
match self.backend.renderer().import_dmabuf(dmabuf, None) {
|
||||||
Ok(_texture) => Ok(()),
|
Ok(_texture) => true,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
debug!("error importing dmabuf: {err:?}");
|
debug!("error importing dmabuf: {err:?}");
|
||||||
Err(())
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ipc_outputs(&self) -> Rc<RefCell<HashMap<String, niri_ipc::Output>>> {
|
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
||||||
self.ipc_outputs.clone()
|
self.ipc_outputs.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enabled_outputs(&self) -> Arc<Mutex<HashMap<String, Output>>> {
|
|
||||||
self.enabled_outputs.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use niri_ipc::Action;
|
||||||
|
|
||||||
|
use crate::utils::version;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(author, version = version(), about, long_about = None)]
|
||||||
|
#[command(args_conflicts_with_subcommands = true)]
|
||||||
|
#[command(subcommand_value_name = "SUBCOMMAND")]
|
||||||
|
#[command(subcommand_help_heading = "Subcommands")]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
/// Import environment globally to systemd and D-Bus, run D-Bus services.
|
||||||
|
///
|
||||||
|
/// Set this flag in a systemd service started by your display manager, or when running
|
||||||
|
/// manually as your main compositor instance. Do not set when running as a nested window, or
|
||||||
|
/// on a TTY as your non-main compositor instance, to avoid messing up the global environment.
|
||||||
|
#[arg(long)]
|
||||||
|
pub session: bool,
|
||||||
|
/// Command to run upon compositor startup.
|
||||||
|
#[arg(last = true)]
|
||||||
|
pub command: Vec<OsString>,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub subcommand: Option<Sub>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Sub {
|
||||||
|
/// Validate the config file.
|
||||||
|
Validate {
|
||||||
|
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Communicate with the running niri instance.
|
||||||
|
Msg {
|
||||||
|
#[command(subcommand)]
|
||||||
|
msg: Msg,
|
||||||
|
/// Format output as JSON.
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
|
/// Cause a panic to check if the backtraces are good.
|
||||||
|
Panic,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Msg {
|
||||||
|
/// Print the version of the running niri instance.
|
||||||
|
Version,
|
||||||
|
/// List connected outputs.
|
||||||
|
Outputs,
|
||||||
|
/// Print information about the focused window.
|
||||||
|
FocusedWindow,
|
||||||
|
/// Perform an action.
|
||||||
|
Action {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: Action,
|
||||||
|
},
|
||||||
|
/// Request an error from the running niri instance.
|
||||||
|
RequestError,
|
||||||
|
}
|
||||||
+6
-20
@@ -8,8 +8,7 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::texture::TextureBuffer;
|
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
|
||||||
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus};
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
use smithay::utils::{IsAlive, Logical, Physical, Point, Transform};
|
||||||
@@ -224,7 +223,7 @@ pub enum RenderCursor {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextureCache = HashMap<(CursorIcon, i32), Vec<Option<TextureBuffer<GlesTexture>>>>;
|
type TextureCache = HashMap<(CursorIcon, i32), Vec<MemoryRenderBuffer>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CursorTextureCache {
|
pub struct CursorTextureCache {
|
||||||
@@ -238,12 +237,11 @@ impl CursorTextureCache {
|
|||||||
|
|
||||||
pub fn get(
|
pub fn get(
|
||||||
&self,
|
&self,
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
icon: CursorIcon,
|
icon: CursorIcon,
|
||||||
scale: i32,
|
scale: i32,
|
||||||
cursor: &XCursor,
|
cursor: &XCursor,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
) -> Option<TextureBuffer<GlesTexture>> {
|
) -> MemoryRenderBuffer {
|
||||||
self.cache
|
self.cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.entry((icon, scale))
|
.entry((icon, scale))
|
||||||
@@ -252,26 +250,14 @@ impl CursorTextureCache {
|
|||||||
.frames()
|
.frames()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|frame| {
|
.map(|frame| {
|
||||||
let _span = tracy_client::span!("create TextureBuffer");
|
MemoryRenderBuffer::from_slice(
|
||||||
|
|
||||||
let buffer = TextureBuffer::from_memory(
|
|
||||||
renderer,
|
|
||||||
&frame.pixels_rgba,
|
&frame.pixels_rgba,
|
||||||
Fourcc::Abgr8888,
|
Fourcc::Argb8888,
|
||||||
(frame.width as i32, frame.height as i32),
|
(frame.width as i32, frame.height as i32),
|
||||||
false,
|
|
||||||
scale,
|
scale,
|
||||||
Transform::Normal,
|
Transform::Normal,
|
||||||
None,
|
None,
|
||||||
);
|
)
|
||||||
|
|
||||||
match buffer {
|
|
||||||
Ok(x) => Some(x),
|
|
||||||
Err(err) => {
|
|
||||||
warn!("error creating a cursor texture: {err:?}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})[idx]
|
})[idx]
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use zbus::fdo::{self, RequestNameFlags};
|
||||||
|
use zbus::names::{OwnedUniqueName, UniqueName};
|
||||||
|
use zbus::zvariant::NoneValue;
|
||||||
|
use zbus::{dbus_interface, MessageHeader, Task};
|
||||||
|
|
||||||
|
use super::Start;
|
||||||
|
|
||||||
|
pub struct ScreenSaver {
|
||||||
|
is_inhibited: Arc<AtomicBool>,
|
||||||
|
is_broken: Arc<AtomicBool>,
|
||||||
|
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
|
||||||
|
counter: u32,
|
||||||
|
monitor_task: Arc<OnceLock<Task<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[dbus_interface(name = "org.freedesktop.ScreenSaver")]
|
||||||
|
impl ScreenSaver {
|
||||||
|
async fn inhibit(
|
||||||
|
&mut self,
|
||||||
|
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||||
|
application_name: &str,
|
||||||
|
reason_for_inhibit: &str,
|
||||||
|
) -> fdo::Result<u32> {
|
||||||
|
trace!(
|
||||||
|
"fdo inhibit, app: `{application_name}`, reason: `{reason_for_inhibit}`, owner: {:?}",
|
||||||
|
hdr.sender()
|
||||||
|
);
|
||||||
|
|
||||||
|
let Ok(Some(name)) = hdr.sender() else {
|
||||||
|
return Err(fdo::Error::Failed(String::from("no sender")));
|
||||||
|
};
|
||||||
|
let name = OwnedUniqueName::from(name.to_owned());
|
||||||
|
|
||||||
|
let mut inhibitors = self.inhibitors.lock().unwrap();
|
||||||
|
|
||||||
|
let mut cookie = None;
|
||||||
|
for _ in 0..3 {
|
||||||
|
// Start from 1 because some clients don't like 0.
|
||||||
|
self.counter = self.counter.wrapping_add(1);
|
||||||
|
if self.counter == 0 {
|
||||||
|
self.counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Entry::Vacant(entry) = inhibitors.entry(self.counter) {
|
||||||
|
entry.insert(name);
|
||||||
|
self.is_inhibited.store(true, Ordering::SeqCst);
|
||||||
|
cookie = Some(self.counter);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie.ok_or_else(|| fdo::Error::Failed(String::from("no available cookie")))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn un_inhibit(&mut self, cookie: u32) -> fdo::Result<()> {
|
||||||
|
trace!("fdo uninhibit, cookie: {cookie}");
|
||||||
|
|
||||||
|
let mut inhibitors = self.inhibitors.lock().unwrap();
|
||||||
|
|
||||||
|
if inhibitors.remove(&cookie).is_some() {
|
||||||
|
if inhibitors.is_empty() {
|
||||||
|
self.is_inhibited.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(fdo::Error::Failed(String::from("invalid cookie")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenSaver {
|
||||||
|
pub fn new(is_inhibited: Arc<AtomicBool>) -> Self {
|
||||||
|
Self {
|
||||||
|
is_inhibited,
|
||||||
|
is_broken: Arc::new(AtomicBool::new(false)),
|
||||||
|
inhibitors: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
counter: 0,
|
||||||
|
monitor_task: Arc::new(OnceLock::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn monitor_disappeared_clients(
|
||||||
|
conn: &zbus::Connection,
|
||||||
|
is_inhibited: Arc<AtomicBool>,
|
||||||
|
inhibitors: Arc<Mutex<HashMap<u32, OwnedUniqueName>>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let proxy = fdo::DBusProxy::new(conn)
|
||||||
|
.await
|
||||||
|
.context("error creating a DBusProxy")?;
|
||||||
|
|
||||||
|
let mut stream = proxy
|
||||||
|
.receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())])
|
||||||
|
.await
|
||||||
|
.context("error creating a NameOwnerChanged stream")?;
|
||||||
|
|
||||||
|
while let Some(signal) = stream.next().await {
|
||||||
|
let args = signal
|
||||||
|
.args()
|
||||||
|
.context("error retrieving NameOwnerChanged args")?;
|
||||||
|
|
||||||
|
let Some(name) = &**args.old_owner() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.new_owner().is_none() {
|
||||||
|
trace!("fdo ScreenSaver client disappeared: {name}");
|
||||||
|
|
||||||
|
let mut inhibitors = inhibitors.lock().unwrap();
|
||||||
|
inhibitors.retain(|_, owner| owner != name);
|
||||||
|
is_inhibited.store(!inhibitors.is_empty(), Ordering::SeqCst);
|
||||||
|
} else {
|
||||||
|
error!("non-null new_owner should've been filtered out");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Start for ScreenSaver {
|
||||||
|
fn start(self) -> anyhow::Result<zbus::blocking::Connection> {
|
||||||
|
let is_inhibited = self.is_inhibited.clone();
|
||||||
|
let is_broken = self.is_broken.clone();
|
||||||
|
let inhibitors = self.inhibitors.clone();
|
||||||
|
let monitor_task = self.monitor_task.clone();
|
||||||
|
|
||||||
|
let conn = zbus::blocking::Connection::session()?;
|
||||||
|
let flags = RequestNameFlags::AllowReplacement
|
||||||
|
| RequestNameFlags::ReplaceExisting
|
||||||
|
| RequestNameFlags::DoNotQueue;
|
||||||
|
|
||||||
|
conn.object_server()
|
||||||
|
.at("/org/freedesktop/ScreenSaver", self)?;
|
||||||
|
conn.request_name_with_flags("org.freedesktop.ScreenSaver", flags)?;
|
||||||
|
|
||||||
|
let async_conn = conn.inner();
|
||||||
|
let future = {
|
||||||
|
let conn = async_conn.clone();
|
||||||
|
async move {
|
||||||
|
if let Err(err) =
|
||||||
|
monitor_disappeared_clients(&conn, is_inhibited.clone(), inhibitors.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!("error monitoring org.freedesktop.ScreenSaver clients: {err:?}");
|
||||||
|
is_broken.store(true, Ordering::SeqCst);
|
||||||
|
is_inhibited.store(false, Ordering::SeqCst);
|
||||||
|
inhibitors.lock().unwrap().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let task = async_conn
|
||||||
|
.executor()
|
||||||
|
.spawn(future, "monitor disappearing clients");
|
||||||
|
monitor_task.set(task).unwrap();
|
||||||
|
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use smithay::reexports::calloop;
|
|
||||||
use zbus::dbus_interface;
|
use zbus::dbus_interface;
|
||||||
use zbus::fdo::{self, RequestNameFlags};
|
use zbus::fdo::{self, RequestNameFlags};
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -1,9 +1,9 @@
|
|||||||
use smithay::reexports::calloop;
|
|
||||||
use zbus::blocking::Connection;
|
use zbus::blocking::Connection;
|
||||||
use zbus::Interface;
|
use zbus::Interface;
|
||||||
|
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
|
|
||||||
|
pub mod freedesktop_screensaver;
|
||||||
pub mod gnome_shell_screenshot;
|
pub mod gnome_shell_screenshot;
|
||||||
pub mod mutter_display_config;
|
pub mod mutter_display_config;
|
||||||
pub mod mutter_service_channel;
|
pub mod mutter_service_channel;
|
||||||
@@ -13,6 +13,7 @@ pub mod mutter_screen_cast;
|
|||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
use mutter_screen_cast::ScreenCast;
|
use mutter_screen_cast::ScreenCast;
|
||||||
|
|
||||||
|
use self::freedesktop_screensaver::ScreenSaver;
|
||||||
use self::mutter_display_config::DisplayConfig;
|
use self::mutter_display_config::DisplayConfig;
|
||||||
use self::mutter_service_channel::ServiceChannel;
|
use self::mutter_service_channel::ServiceChannel;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ trait Start: Interface {
|
|||||||
pub struct DBusServers {
|
pub struct DBusServers {
|
||||||
pub conn_service_channel: Option<Connection>,
|
pub conn_service_channel: Option<Connection>,
|
||||||
pub conn_display_config: Option<Connection>,
|
pub conn_display_config: Option<Connection>,
|
||||||
|
pub conn_screen_saver: Option<Connection>,
|
||||||
pub conn_screen_shot: Option<Connection>,
|
pub conn_screen_shot: Option<Connection>,
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
pub conn_screen_cast: Option<Connection>,
|
pub conn_screen_cast: Option<Connection>,
|
||||||
@@ -45,9 +47,12 @@ impl DBusServers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
if is_session_instance || config.debug.dbus_interfaces_in_non_session_instances {
|
||||||
let display_config = DisplayConfig::new(backend.enabled_outputs());
|
let display_config = DisplayConfig::new(backend.ipc_outputs());
|
||||||
dbus.conn_display_config = try_start(display_config);
|
dbus.conn_display_config = try_start(display_config);
|
||||||
|
|
||||||
|
let screen_saver = ScreenSaver::new(niri.is_fdo_idle_inhibited.clone());
|
||||||
|
dbus.conn_screen_saver = try_start(screen_saver);
|
||||||
|
|
||||||
let (to_niri, from_screenshot) = calloop::channel::channel();
|
let (to_niri, from_screenshot) = calloop::channel::channel();
|
||||||
let (to_screenshot, from_niri) = async_channel::unbounded();
|
let (to_screenshot, from_niri) = async_channel::unbounded();
|
||||||
niri.event_loop
|
niri.event_loop
|
||||||
@@ -62,7 +67,7 @@ impl DBusServers {
|
|||||||
dbus.conn_screen_shot = try_start(screenshot);
|
dbus.conn_screen_shot = try_start(screenshot);
|
||||||
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
{
|
if niri.pipewire.is_some() {
|
||||||
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
let (to_niri, from_screen_cast) = calloop::channel::channel();
|
||||||
niri.event_loop
|
niri.event_loop
|
||||||
.insert_source(from_screen_cast, {
|
.insert_source(from_screen_cast, {
|
||||||
@@ -75,8 +80,10 @@ impl DBusServers {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let screen_cast = ScreenCast::new(backend.enabled_outputs(), to_niri);
|
let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri);
|
||||||
dbus.conn_screen_cast = try_start(screen_cast);
|
dbus.conn_screen_cast = try_start(screen_cast);
|
||||||
|
} else {
|
||||||
|
warn!("disabling screencast support because we couldn't start PipeWire");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ use std::collections::HashMap;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use smithay::output::Output;
|
|
||||||
use zbus::fdo::RequestNameFlags;
|
use zbus::fdo::RequestNameFlags;
|
||||||
use zbus::zvariant::{self, OwnedValue, Type};
|
use zbus::zvariant::{self, OwnedValue, Type};
|
||||||
use zbus::{dbus_interface, fdo};
|
use zbus::{dbus_interface, fdo, SignalContext};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
use crate::backend::IpcOutputMap;
|
||||||
|
|
||||||
pub struct DisplayConfig {
|
pub struct DisplayConfig {
|
||||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Type)]
|
#[derive(Serialize, Type)]
|
||||||
@@ -53,12 +53,14 @@ impl DisplayConfig {
|
|||||||
HashMap<String, OwnedValue>,
|
HashMap<String, OwnedValue>,
|
||||||
)> {
|
)> {
|
||||||
// Construct the DBus response.
|
// Construct the DBus response.
|
||||||
let mut monitors: Vec<Monitor> = self
|
let mut monitors: Vec<(Monitor, LogicalMonitor)> = self
|
||||||
.enabled_outputs
|
.ipc_outputs
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.keys()
|
.iter()
|
||||||
.map(|c| {
|
// Take only enabled outputs.
|
||||||
|
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
|
||||||
|
.map(|(c, output)| {
|
||||||
// Loosely matches the check in Mutter.
|
// Loosely matches the check in Mutter.
|
||||||
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||||
|
|
||||||
@@ -78,46 +80,91 @@ impl DisplayConfig {
|
|||||||
OwnedValue::from(is_laptop_panel),
|
OwnedValue::from(is_laptop_panel),
|
||||||
);
|
);
|
||||||
|
|
||||||
Monitor {
|
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.;
|
||||||
|
|
||||||
|
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()]
|
||||||
|
.properties
|
||||||
|
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||||
|
|
||||||
|
let monitor = Monitor {
|
||||||
names: (c.clone(), String::new(), String::new(), serial),
|
names: (c.clone(), String::new(), String::new(), serial),
|
||||||
modes: vec![],
|
modes,
|
||||||
properties,
|
properties,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let logical = output.logical.as_ref().unwrap();
|
||||||
|
|
||||||
|
let transform = match logical.transform {
|
||||||
|
niri_ipc::Transform::Normal => 0,
|
||||||
|
niri_ipc::Transform::_90 => 1,
|
||||||
|
niri_ipc::Transform::_180 => 2,
|
||||||
|
niri_ipc::Transform::_270 => 3,
|
||||||
|
niri_ipc::Transform::Flipped => 4,
|
||||||
|
niri_ipc::Transform::Flipped90 => 5,
|
||||||
|
niri_ipc::Transform::Flipped180 => 6,
|
||||||
|
niri_ipc::Transform::Flipped270 => 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
let logical_monitor = LogicalMonitor {
|
||||||
|
x: logical.x,
|
||||||
|
y: logical.y,
|
||||||
|
scale: logical.scale,
|
||||||
|
transform,
|
||||||
|
is_primary: false,
|
||||||
|
monitors: vec![monitor.names.clone()],
|
||||||
|
properties: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(monitor, logical_monitor)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Sort the built-in monitor first, then by connector name.
|
// Sort the built-in monitor first, then by connector name.
|
||||||
monitors.sort_unstable_by(|a, b| {
|
monitors.sort_unstable_by(|a, b| {
|
||||||
let a_is_builtin = a.properties.contains_key("display-name");
|
let a_is_builtin = a.0.properties.contains_key("display-name");
|
||||||
let b_is_builtin = b.properties.contains_key("display-name");
|
let b_is_builtin = b.0.properties.contains_key("display-name");
|
||||||
a_is_builtin
|
a_is_builtin
|
||||||
.cmp(&b_is_builtin)
|
.cmp(&b_is_builtin)
|
||||||
.reverse()
|
.reverse()
|
||||||
.then_with(|| a.names.0.cmp(&b.names.0))
|
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
|
||||||
});
|
});
|
||||||
|
|
||||||
let logical_monitors = monitors
|
let (monitors, logical_monitors) = monitors.into_iter().unzip();
|
||||||
.iter()
|
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
||||||
.map(|m| LogicalMonitor {
|
Ok((0, monitors, logical_monitors, properties))
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
scale: 1.,
|
|
||||||
transform: 0,
|
|
||||||
is_primary: false,
|
|
||||||
monitors: vec![m.names.clone()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok((0, monitors, logical_monitors, HashMap::new()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: monitors-changed signal.
|
#[dbus_interface(signal)]
|
||||||
|
pub async fn monitors_changed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayConfig {
|
impl DisplayConfig {
|
||||||
pub fn new(enabled_outputs: Arc<Mutex<HashMap<String, Output>>>) -> Self {
|
pub fn new(ipc_outputs: Arc<Mutex<IpcOutputMap>>) -> Self {
|
||||||
Self { enabled_outputs }
|
Self { ipc_outputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::calloop;
|
|
||||||
use zbus::fdo::RequestNameFlags;
|
use zbus::fdo::RequestNameFlags;
|
||||||
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
|
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
|
||||||
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
|
||||||
|
|
||||||
use super::Start;
|
use super::Start;
|
||||||
|
use crate::backend::IpcOutputMap;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ScreenCast {
|
pub struct ScreenCast {
|
||||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
|
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
|
||||||
@@ -23,10 +23,11 @@ pub struct ScreenCast {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
id: usize,
|
id: usize,
|
||||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
|
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
|
||||||
|
stopped: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
|
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
|
||||||
@@ -48,16 +49,26 @@ struct RecordMonitorProperties {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Stream {
|
pub struct Stream {
|
||||||
output: Output,
|
// FIXME: update on scale changes and whatnot.
|
||||||
|
output: niri_ipc::Output,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
was_started: Arc<AtomicBool>,
|
was_started: Arc<AtomicBool>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SerializeDict, Type, Value)]
|
||||||
|
#[zvariant(signature = "dict")]
|
||||||
|
struct StreamParameters {
|
||||||
|
/// Position of the stream in logical coordinates.
|
||||||
|
position: (i32, i32),
|
||||||
|
/// Size of the stream in logical coordinates.
|
||||||
|
size: (i32, i32),
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ScreenCastToNiri {
|
pub enum ScreenCastToNiri {
|
||||||
StartCast {
|
StartCast {
|
||||||
session_id: usize,
|
session_id: usize,
|
||||||
output: Output,
|
output: String,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
signal_ctx: SignalContext<'static>,
|
signal_ctx: SignalContext<'static>,
|
||||||
},
|
},
|
||||||
@@ -85,11 +96,7 @@ impl ScreenCast {
|
|||||||
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
|
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
|
||||||
let path = OwnedObjectPath::try_from(path).unwrap();
|
let path = OwnedObjectPath::try_from(path).unwrap();
|
||||||
|
|
||||||
let session = Session::new(
|
let session = Session::new(session_id, self.ipc_outputs.clone(), self.to_niri.clone());
|
||||||
session_id,
|
|
||||||
self.enabled_outputs.clone(),
|
|
||||||
self.to_niri.clone(),
|
|
||||||
);
|
|
||||||
match server.at(&path, session.clone()).await {
|
match server.at(&path, session.clone()).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let iface = server.interface(&path).await.unwrap();
|
let iface = server.interface(&path).await.unwrap();
|
||||||
@@ -129,6 +136,11 @@ impl Session {
|
|||||||
) {
|
) {
|
||||||
debug!("stop");
|
debug!("stop");
|
||||||
|
|
||||||
|
if self.stopped.swap(true, Ordering::SeqCst) {
|
||||||
|
// Already stopped.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Session::closed(&ctxt).await.unwrap();
|
Session::closed(&ctxt).await.unwrap();
|
||||||
|
|
||||||
if let Err(err) = self.to_niri.send(ScreenCastToNiri::StopCast {
|
if let Err(err) = self.to_niri.send(ScreenCastToNiri::StopCast {
|
||||||
@@ -156,10 +168,14 @@ impl Session {
|
|||||||
) -> fdo::Result<OwnedObjectPath> {
|
) -> fdo::Result<OwnedObjectPath> {
|
||||||
debug!(connector, ?properties, "record_monitor");
|
debug!(connector, ?properties, "record_monitor");
|
||||||
|
|
||||||
let Some(output) = self.enabled_outputs.lock().unwrap().get(connector).cloned() else {
|
let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
|
||||||
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if output.logical.is_none() {
|
||||||
|
return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
static NUMBER: AtomicUsize = AtomicUsize::new(0);
|
static NUMBER: AtomicUsize = AtomicUsize::new(0);
|
||||||
let path = format!(
|
let path = format!(
|
||||||
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
|
||||||
@@ -169,7 +185,7 @@ impl Session {
|
|||||||
|
|
||||||
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
let cursor_mode = properties.cursor_mode.unwrap_or_default();
|
||||||
|
|
||||||
let stream = Stream::new(output, cursor_mode, self.to_niri.clone());
|
let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone());
|
||||||
match server.at(&path, stream.clone()).await {
|
match server.at(&path, stream.clone()).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let iface = server.interface(&path).await.unwrap();
|
let iface = server.interface(&path).await.unwrap();
|
||||||
@@ -195,15 +211,24 @@ impl Stream {
|
|||||||
#[dbus_interface(signal)]
|
#[dbus_interface(signal)]
|
||||||
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
|
||||||
-> zbus::Result<()>;
|
-> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[dbus_interface(property)]
|
||||||
|
async fn parameters(&self) -> StreamParameters {
|
||||||
|
let logical = self.output.logical.as_ref().unwrap();
|
||||||
|
StreamParameters {
|
||||||
|
position: (logical.x, logical.y),
|
||||||
|
size: (logical.width as i32, logical.height as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScreenCast {
|
impl ScreenCast {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled_outputs,
|
ipc_outputs,
|
||||||
to_niri,
|
to_niri,
|
||||||
sessions: Arc::new(Mutex::new(vec![])),
|
sessions: Arc::new(Mutex::new(vec![])),
|
||||||
}
|
}
|
||||||
@@ -228,14 +253,15 @@ impl Start for ScreenCast {
|
|||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: usize,
|
id: usize,
|
||||||
enabled_outputs: Arc<Mutex<HashMap<String, Output>>>,
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
enabled_outputs,
|
ipc_outputs,
|
||||||
streams: Arc::new(Mutex::new(vec![])),
|
streams: Arc::new(Mutex::new(vec![])),
|
||||||
to_niri,
|
to_niri,
|
||||||
|
stopped: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +276,7 @@ impl Drop for Session {
|
|||||||
|
|
||||||
impl Stream {
|
impl Stream {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
output: Output,
|
output: niri_ipc::Output,
|
||||||
cursor_mode: CursorMode,
|
cursor_mode: CursorMode,
|
||||||
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -269,7 +295,7 @@ impl Stream {
|
|||||||
|
|
||||||
let msg = ScreenCastToNiri::StartCast {
|
let msg = ScreenCastToNiri::StartCast {
|
||||||
session_id,
|
session_id,
|
||||||
output: self.output.clone(),
|
output: self.output.name.clone(),
|
||||||
cursor_mode: self.cursor_mode,
|
cursor_mode: self.cursor_mode,
|
||||||
signal_ctx: ctxt,
|
signal_ctx: ctxt,
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-2
@@ -7,10 +7,11 @@ use crate::utils::get_monotonic_time;
|
|||||||
pub struct FrameClock {
|
pub struct FrameClock {
|
||||||
last_presentation_time: Option<Duration>,
|
last_presentation_time: Option<Duration>,
|
||||||
refresh_interval_ns: Option<NonZeroU64>,
|
refresh_interval_ns: Option<NonZeroU64>,
|
||||||
|
vrr: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrameClock {
|
impl FrameClock {
|
||||||
pub fn new(refresh_interval: Option<Duration>) -> Self {
|
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
|
||||||
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
|
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
|
||||||
assert_eq!(interval.as_secs(), 0);
|
assert_eq!(interval.as_secs(), 0);
|
||||||
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
|
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
|
||||||
@@ -21,6 +22,7 @@ impl FrameClock {
|
|||||||
Self {
|
Self {
|
||||||
last_presentation_time: None,
|
last_presentation_time: None,
|
||||||
refresh_interval_ns,
|
refresh_interval_ns,
|
||||||
|
vrr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +31,15 @@ impl FrameClock {
|
|||||||
.map(|r| Duration::from_nanos(r.get()))
|
.map(|r| Duration::from_nanos(r.get()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_vrr(&mut self, vrr: bool) {
|
||||||
|
if self.vrr == vrr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.vrr = vrr;
|
||||||
|
self.last_presentation_time = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn presented(&mut self, presentation_time: Duration) {
|
pub fn presented(&mut self, presentation_time: Duration) {
|
||||||
if presentation_time.is_zero() {
|
if presentation_time.is_zero() {
|
||||||
// Not interested in these.
|
// Not interested in these.
|
||||||
@@ -71,6 +82,13 @@ impl FrameClock {
|
|||||||
let since_last_ns =
|
let since_last_ns =
|
||||||
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
|
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
|
||||||
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
|
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
|
||||||
last_presentation_time + Duration::from_nanos(to_next_ns)
|
|
||||||
|
// If VRR is enabled and more than one frame passed since last presentation, assume that we
|
||||||
|
// can present immediately.
|
||||||
|
if self.vrr && to_next_ns > refresh_interval_ns {
|
||||||
|
now
|
||||||
|
} else {
|
||||||
|
last_presentation_time + Duration::from_nanos(to_next_ns)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-22
@@ -16,9 +16,9 @@ use smithay::wayland::dmabuf::get_dmabuf;
|
|||||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||||
use smithay::{delegate_compositor, delegate_shm};
|
use smithay::{delegate_compositor, delegate_shm};
|
||||||
|
|
||||||
use super::xdg_shell;
|
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||||
use crate::niri::{ClientState, State};
|
use crate::niri::{ClientState, State};
|
||||||
use crate::utils::clone2;
|
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||||
|
|
||||||
impl CompositorHandler for State {
|
impl CompositorHandler for State {
|
||||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||||
@@ -75,7 +75,7 @@ impl CompositorHandler for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&mut self, surface: &WlSurface) {
|
fn commit(&mut self, surface: &WlSurface) {
|
||||||
@@ -93,45 +93,145 @@ impl CompositorHandler for State {
|
|||||||
root_surface = parent;
|
root_surface = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the cached root surface.
|
||||||
|
self.niri
|
||||||
|
.root_surface
|
||||||
|
.insert(surface.clone(), root_surface.clone());
|
||||||
|
|
||||||
if surface == &root_surface {
|
if surface == &root_surface {
|
||||||
// This is a root surface commit. It might have mapped a previously-unmapped toplevel.
|
// 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()) {
|
if let Entry::Occupied(entry) = self.niri.unmapped_windows.entry(surface.clone()) {
|
||||||
let is_mapped =
|
let is_mapped =
|
||||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
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 {
|
||||||
// The toplevel got mapped.
|
// The toplevel got mapped.
|
||||||
let window = entry.remove();
|
let Unmapped { window, state } = entry.remove();
|
||||||
|
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
|
|
||||||
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
|
let toplevel = window.toplevel().expect("no X11 support");
|
||||||
{
|
|
||||||
self.niri.queue_redraw(output);
|
let (rules, width, is_full_width, output) =
|
||||||
|
if let InitialConfigureState::Configured {
|
||||||
|
rules,
|
||||||
|
width,
|
||||||
|
is_full_width,
|
||||||
|
output,
|
||||||
|
} = state
|
||||||
|
{
|
||||||
|
// Check that the output is still connected.
|
||||||
|
let output =
|
||||||
|
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
|
||||||
|
|
||||||
|
(rules, width, is_full_width, output)
|
||||||
|
} else {
|
||||||
|
error!("window map must happen after initial configure");
|
||||||
|
(ResolvedWindowRules::empty(), None, false, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = toplevel
|
||||||
|
.parent()
|
||||||
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
|
// Only consider the parent if we configured the window for the same
|
||||||
|
// output.
|
||||||
|
//
|
||||||
|
// Normally when we're following the parent, the configured output will be
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
.map(|(mapped, _)| mapped.window.clone());
|
||||||
|
|
||||||
|
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
|
||||||
|
let mapped = Mapped::new(window, rules, hook);
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
|
||||||
|
let output = if let Some(p) = parent {
|
||||||
|
// Open dialogs immediately to the right of their parent window.
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.add_window_right_of(&p, mapped, width, is_full_width)
|
||||||
|
} else if let Some(output) = &output {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.add_window_on_output(output, mapped, width, is_full_width);
|
||||||
|
Some(output)
|
||||||
|
} else {
|
||||||
|
self.niri.layout.add_window(mapped, width, is_full_width)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(output) = output.cloned() {
|
||||||
|
self.niri.layout.start_open_animation_for_window(&window);
|
||||||
|
|
||||||
|
let new_active_window =
|
||||||
|
self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||||
|
if new_active_window == Some(&window) {
|
||||||
|
self.maybe_warp_cursor_to_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The toplevel remains unmapped.
|
// The toplevel remains unmapped.
|
||||||
let window = entry.get();
|
let unmapped = entry.get();
|
||||||
xdg_shell::send_initial_configure_if_needed(window.toplevel());
|
if unmapped.needs_initial_configure() {
|
||||||
|
let toplevel = unmapped.window.toplevel().expect("no x11 support").clone();
|
||||||
|
self.queue_initial_configure(toplevel);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a commit of a previously-mapped root or a non-toplevel root.
|
// This is a commit of a previously-mapped root or a non-toplevel root.
|
||||||
if let Some(win_out) = self.niri.layout.find_window_and_output(surface) {
|
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(surface) {
|
||||||
let (window, output) = clone2(win_out);
|
let window = mapped.window.clone();
|
||||||
|
let output = output.clone();
|
||||||
window.on_commit();
|
|
||||||
|
|
||||||
// This is a commit of a previously-mapped toplevel.
|
// This is a commit of a previously-mapped toplevel.
|
||||||
let is_mapped =
|
let is_mapped =
|
||||||
with_renderer_surface_state(surface, |state| state.buffer().is_some());
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must start the close animation before window.on_commit().
|
||||||
|
if !is_mapped {
|
||||||
|
self.backend.with_primary_renderer(|renderer| {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.start_close_animation_for_window(renderer, &window);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.on_commit();
|
||||||
|
|
||||||
if !is_mapped {
|
if !is_mapped {
|
||||||
// The toplevel got unmapped.
|
// The toplevel got unmapped.
|
||||||
|
//
|
||||||
|
// Test client: wleird-unmap.
|
||||||
|
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||||
|
let was_active = active_window == Some(&window);
|
||||||
|
|
||||||
self.niri.layout.remove_window(&window);
|
self.niri.layout.remove_window(&window);
|
||||||
self.niri.unmapped_windows.insert(surface.clone(), window);
|
|
||||||
self.niri.queue_redraw(output);
|
if was_active {
|
||||||
|
self.maybe_warp_cursor_to_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newly-unmapped toplevels must perform the initial commit-configure sequence
|
||||||
|
// afresh.
|
||||||
|
let unmapped = Unmapped::new(window);
|
||||||
|
self.niri.unmapped_windows.insert(surface.clone(), unmapped);
|
||||||
|
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +241,7 @@ impl CompositorHandler for State {
|
|||||||
// Popup placement depends on window size which might have changed.
|
// Popup placement depends on window size which might have changed.
|
||||||
self.update_reactive_popups(&window, &output);
|
self.update_reactive_popups(&window, &output);
|
||||||
|
|
||||||
self.niri.queue_redraw(output);
|
self.niri.queue_redraw(&output);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +250,12 @@ impl CompositorHandler for State {
|
|||||||
|
|
||||||
// This is a commit of a non-root or a non-toplevel root.
|
// This is a commit of a non-root or a non-toplevel root.
|
||||||
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
let root_window_output = self.niri.layout.find_window_and_output(&root_surface);
|
||||||
if let Some((window, output)) = root_window_output.map(clone2) {
|
if let Some((mapped, output)) = root_window_output {
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
let output = output.clone();
|
||||||
window.on_commit();
|
window.on_commit();
|
||||||
self.niri.layout.update_window(&window);
|
self.niri.layout.update_window(&window);
|
||||||
self.niri.queue_redraw(output);
|
self.niri.queue_redraw(&output);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +263,7 @@ impl CompositorHandler for State {
|
|||||||
self.popups_handle_commit(surface);
|
self.popups_handle_commit(surface);
|
||||||
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
||||||
if let Some(output) = self.output_for_popup(&popup) {
|
if let Some(output) = self.output_for_popup(&popup) {
|
||||||
self.niri.queue_redraw(output.clone());
|
self.niri.queue_redraw(&output.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,13 +288,31 @@ impl CompositorHandler for State {
|
|||||||
for (output, state) in &self.niri.output_state {
|
for (output, state) in &self.niri.output_state {
|
||||||
if let Some(lock_surface) = &state.lock_surface {
|
if let Some(lock_surface) = &state.lock_surface {
|
||||||
if lock_surface.wl_surface() == surface {
|
if lock_surface.wl_surface() == surface {
|
||||||
self.niri.queue_redraw(output.clone());
|
self.niri.queue_redraw(&output.clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn destroyed(&mut self, surface: &WlSurface) {
|
||||||
|
// Clients may destroy their subsurfaces before the main surface. Ensure we have a snapshot
|
||||||
|
// when that happens, so that the closing animation includes all these subsurfaces.
|
||||||
|
//
|
||||||
|
// Test client: alacritty with CSD.
|
||||||
|
if let Some(root) = self.niri.root_surface.get(surface) {
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
|
||||||
|
self.backend.with_primary_renderer(|renderer| {
|
||||||
|
mapped.store_unmap_snapshot_if_empty(renderer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.niri
|
||||||
|
.root_surface
|
||||||
|
.retain(|k, v| k != surface && v != surface);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferHandler for State {
|
impl BufferHandler for State {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl WlrLayerShellHandler for State {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
if let Some(output) = output {
|
if let Some(output) = output {
|
||||||
self.niri.output_resized(output);
|
self.niri.output_resized(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,6 @@ impl State {
|
|||||||
}
|
}
|
||||||
drop(map);
|
drop(map);
|
||||||
|
|
||||||
self.niri.output_resized(output);
|
self.niri.output_resized(&output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+221
-15
@@ -9,10 +9,13 @@ use std::sync::Arc;
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
|
use smithay::backend::drm::DrmNode;
|
||||||
|
use smithay::backend::input::TabletToolDescriptor;
|
||||||
use smithay::desktop::{PopupKind, PopupManager};
|
use smithay::desktop::{PopupKind, PopupManager};
|
||||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
@@ -20,7 +23,13 @@ use smithay::reexports::wayland_server::Resource;
|
|||||||
use smithay::utils::{Logical, Rectangle, Size};
|
use smithay::utils::{Logical, Rectangle, Size};
|
||||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||||
|
use smithay::wayland::drm_lease::{
|
||||||
|
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||||
|
};
|
||||||
|
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||||
|
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||||
|
use smithay::wayland::output::OutputHandler;
|
||||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||||
use smithay::wayland::security_context::{
|
use smithay::wayland::security_context::{
|
||||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||||
@@ -37,20 +46,29 @@ use smithay::wayland::selection::{SelectionHandler, SelectionTarget};
|
|||||||
use smithay::wayland::session_lock::{
|
use smithay::wayland::session_lock::{
|
||||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
|
||||||
};
|
};
|
||||||
|
use smithay::wayland::tablet_manager::TabletSeatHandler;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||||
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
|
delegate_drm_lease, delegate_idle_inhibit, delegate_idle_notify, delegate_input_method_manager,
|
||||||
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
|
delegate_output, delegate_pointer_constraints, delegate_pointer_gestures,
|
||||||
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
|
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
||||||
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
|
delegate_security_context, delegate_session_lock, delegate_tablet_manager,
|
||||||
|
delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::niri::{ClientState, State};
|
use crate::niri::{ClientState, State};
|
||||||
|
use crate::protocols::foreign_toplevel::{
|
||||||
|
self, ForeignToplevelHandler, ForeignToplevelManagerState,
|
||||||
|
};
|
||||||
|
use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState};
|
||||||
|
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler};
|
||||||
use crate::utils::output_size;
|
use crate::utils::output_size;
|
||||||
|
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
|
||||||
|
|
||||||
impl SeatHandler for State {
|
impl SeatHandler for State {
|
||||||
type KeyboardFocus = WlSurface;
|
type KeyboardFocus = WlSurface;
|
||||||
type PointerFocus = WlSurface;
|
type PointerFocus = WlSurface;
|
||||||
|
type TouchFocus = WlSurface;
|
||||||
|
|
||||||
fn seat_state(&mut self) -> &mut SeatState<State> {
|
fn seat_state(&mut self) -> &mut SeatState<State> {
|
||||||
&mut self.niri.seat_state
|
&mut self.niri.seat_state
|
||||||
@@ -73,14 +91,36 @@ impl SeatHandler for State {
|
|||||||
set_data_device_focus(dh, seat, client.clone());
|
set_data_device_focus(dh, seat, client.clone());
|
||||||
set_primary_focus(dh, seat, client);
|
set_primary_focus(dh, seat, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn led_state_changed(&mut self, _seat: &Seat<Self>, led_state: keyboard::LedState) {
|
||||||
|
let keyboards = self
|
||||||
|
.niri
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.filter(|device| device.has_capability(input::DeviceCapability::Keyboard))
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
for mut keyboard in keyboards {
|
||||||
|
keyboard.led_update(led_state.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delegate_seat!(State);
|
delegate_seat!(State);
|
||||||
delegate_cursor_shape!(State);
|
delegate_cursor_shape!(State);
|
||||||
delegate_tablet_manager!(State);
|
|
||||||
delegate_pointer_gestures!(State);
|
delegate_pointer_gestures!(State);
|
||||||
delegate_relative_pointer!(State);
|
delegate_relative_pointer!(State);
|
||||||
delegate_text_input_manager!(State);
|
delegate_text_input_manager!(State);
|
||||||
|
|
||||||
|
impl TabletSeatHandler for State {
|
||||||
|
fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, image: CursorImageStatus) {
|
||||||
|
// FIXME: tablet tools should have their own cursors.
|
||||||
|
self.niri.cursor_manager.set_cursor_image(image);
|
||||||
|
// FIXME: granular.
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_tablet_manager!(State);
|
||||||
|
|
||||||
impl PointerConstraintsHandler for State {
|
impl PointerConstraintsHandler for State {
|
||||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||||
self.niri.maybe_activate_pointer_constraint(
|
self.niri.maybe_activate_pointer_constraint(
|
||||||
@@ -115,7 +155,7 @@ impl InputMethodHandler for State {
|
|||||||
self.niri
|
self.niri
|
||||||
.layout
|
.layout
|
||||||
.find_window_and_output(parent)
|
.find_window_and_output(parent)
|
||||||
.map(|(window, _)| window.geometry())
|
.map(|(mapped, _)| mapped.window.geometry())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +229,11 @@ impl DataControlHandler for State {
|
|||||||
|
|
||||||
delegate_data_control!(State);
|
delegate_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
delegate_output!(State);
|
delegate_output!(State);
|
||||||
|
|
||||||
delegate_presentation!(State);
|
delegate_presentation!(State);
|
||||||
@@ -204,13 +249,10 @@ impl DmabufHandler for State {
|
|||||||
dmabuf: Dmabuf,
|
dmabuf: Dmabuf,
|
||||||
notifier: ImportNotifier,
|
notifier: ImportNotifier,
|
||||||
) {
|
) {
|
||||||
match self.backend.import_dmabuf(&dmabuf) {
|
if self.backend.import_dmabuf(&dmabuf) {
|
||||||
Ok(_) => {
|
let _ = notifier.successful::<State>();
|
||||||
let _ = notifier.successful::<State>();
|
} else {
|
||||||
}
|
notifier.failed();
|
||||||
Err(_) => {
|
|
||||||
notifier.failed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +310,7 @@ impl SecurityContextHandler for State {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
if let Err(err) = state.niri.display_handle.insert_client(client, data) {
|
||||||
error!("error inserting client: {err}");
|
warn!("error inserting client: {err}");
|
||||||
} else {
|
} else {
|
||||||
trace!("inserted a new restricted client, context={context:?}");
|
trace!("inserted a new restricted client, context={context:?}");
|
||||||
}
|
}
|
||||||
@@ -277,3 +319,167 @@ impl SecurityContextHandler for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate_security_context!(State);
|
delegate_security_context!(State);
|
||||||
|
|
||||||
|
impl IdleNotifierHandler for State {
|
||||||
|
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
|
||||||
|
&mut self.niri.idle_notifier_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_idle_notify!(State);
|
||||||
|
|
||||||
|
impl IdleInhibitHandler for State {
|
||||||
|
fn inhibit(&mut self, surface: WlSurface) {
|
||||||
|
self.niri.idle_inhibiting_surfaces.insert(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninhibit(&mut self, surface: WlSurface) {
|
||||||
|
self.niri.idle_inhibiting_surfaces.remove(&surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_idle_inhibit!(State);
|
||||||
|
|
||||||
|
impl ForeignToplevelHandler for State {
|
||||||
|
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState {
|
||||||
|
&mut self.niri.foreign_toplevel_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate(&mut self, wl_surface: WlSurface) {
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
self.niri.layout.activate_window(&window);
|
||||||
|
self.niri.queue_redraw_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self, wl_surface: WlSurface) {
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||||
|
mapped.toplevel().send_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>) {
|
||||||
|
if let Some((mapped, current_output)) = self.niri.layout.find_window_and_output(&wl_surface)
|
||||||
|
{
|
||||||
|
if !mapped
|
||||||
|
.toplevel()
|
||||||
|
.current_state()
|
||||||
|
.capabilities
|
||||||
|
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_window_to_output(&window, &requested_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.niri.layout.set_fullscreen(&window, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unset_fullscreen(&mut self, wl_surface: WlSurface) {
|
||||||
|
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&wl_surface) {
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
self.niri.layout.set_fullscreen(&window, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_foreign_toplevel!(State);
|
||||||
|
|
||||||
|
impl ScreencopyHandler for State {
|
||||||
|
fn frame(&mut self, screencopy: Screencopy) {
|
||||||
|
if let Err(err) = self
|
||||||
|
.niri
|
||||||
|
.render_for_screencopy(&mut self.backend, screencopy)
|
||||||
|
{
|
||||||
|
warn!("error rendering for screencopy: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_screencopy!(State);
|
||||||
|
|
||||||
|
impl DrmLeaseHandler for State {
|
||||||
|
fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState {
|
||||||
|
self.backend
|
||||||
|
.tty()
|
||||||
|
.get_device_from_node(node)
|
||||||
|
.unwrap()
|
||||||
|
.drm_lease_state
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lease_request(
|
||||||
|
&mut self,
|
||||||
|
node: DrmNode,
|
||||||
|
request: DrmLeaseRequest,
|
||||||
|
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
||||||
|
debug!(
|
||||||
|
"Received lease request for {} connectors",
|
||||||
|
request.connectors.len()
|
||||||
|
);
|
||||||
|
self.backend
|
||||||
|
.tty()
|
||||||
|
.get_device_from_node(node)
|
||||||
|
.unwrap()
|
||||||
|
.lease_request(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) {
|
||||||
|
debug!("Lease success");
|
||||||
|
self.backend
|
||||||
|
.tty()
|
||||||
|
.get_device_from_node(node)
|
||||||
|
.unwrap()
|
||||||
|
.new_lease(lease);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) {
|
||||||
|
debug!("Destroyed lease");
|
||||||
|
self.backend
|
||||||
|
.tty()
|
||||||
|
.get_device_from_node(node)
|
||||||
|
.unwrap()
|
||||||
|
.remove_lease(lease_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_drm_lease!(State);
|
||||||
|
|
||||||
|
delegate_viewporter!(State);
|
||||||
|
|
||||||
|
impl GammaControlHandler for State {
|
||||||
|
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState {
|
||||||
|
&mut self.niri.gamma_control_manager_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_gamma_size(&mut self, output: &Output) -> Option<u32> {
|
||||||
|
match self.backend.tty().get_gamma_size(output) {
|
||||||
|
Ok(0) => None, // Setting gamma is not supported.
|
||||||
|
Ok(size) => Some(size),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"error getting gamma size for output {}: {err:?}",
|
||||||
|
output.name()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()> {
|
||||||
|
match self.backend.tty().set_gamma(output, ramp) {
|
||||||
|
Ok(()) => Some(()),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error setting gamma for output {}: {err:?}", output.name());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_gamma_control!(State);
|
||||||
|
|||||||
+425
-70
@@ -12,7 +12,11 @@ use smithay::reexports::wayland_server::protocol::wl_output;
|
|||||||
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
use smithay::utils::{Logical, Rectangle, Serial};
|
use smithay::utils::{Logical, Rectangle, Serial};
|
||||||
use smithay::wayland::compositor::{send_surface_state, with_states};
|
use smithay::wayland::compositor::{
|
||||||
|
add_pre_commit_hook, send_surface_state, with_states, BufferAssignment, HookId,
|
||||||
|
SurfaceAttributes,
|
||||||
|
};
|
||||||
|
use smithay::wayland::input_method::InputMethodSeat;
|
||||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||||
use smithay::wayland::shell::wlr_layer::Layer;
|
use smithay::wayland::shell::wlr_layer::Layer;
|
||||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||||
@@ -20,10 +24,15 @@ use smithay::wayland::shell::xdg::{
|
|||||||
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
||||||
XdgShellState, XdgToplevelSurfaceData,
|
XdgShellState, XdgToplevelSurfaceData,
|
||||||
};
|
};
|
||||||
use smithay::{delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_shell};
|
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
|
||||||
|
use smithay::{
|
||||||
|
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::layout::workspace::ColumnWidth;
|
||||||
|
use crate::layout::LayoutElement as _;
|
||||||
use crate::niri::{PopupGrabState, State};
|
use crate::niri::{PopupGrabState, State};
|
||||||
use crate::utils::clone2;
|
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
|
||||||
|
|
||||||
impl XdgShellHandler for State {
|
impl XdgShellHandler for State {
|
||||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||||
@@ -32,27 +41,8 @@ impl XdgShellHandler for State {
|
|||||||
|
|
||||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||||
let wl_surface = surface.wl_surface().clone();
|
let wl_surface = surface.wl_surface().clone();
|
||||||
let window = Window::new(surface);
|
let unmapped = Unmapped::new(Window::new_wayland_window(surface));
|
||||||
|
let existing = self.niri.unmapped_windows.insert(wl_surface, unmapped);
|
||||||
// Tell the surface the preferred size and bounds for its likely output.
|
|
||||||
if let Some(ws) = self.niri.layout.active_workspace() {
|
|
||||||
ws.configure_new_window(&window);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
let config = self.niri.config.borrow();
|
|
||||||
if config.prefer_no_csd {
|
|
||||||
window.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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// At the moment of creation, xdg toplevels must have no buffer.
|
|
||||||
let existing = self.niri.unmapped_windows.insert(wl_surface, window);
|
|
||||||
assert!(existing.is_none());
|
assert!(existing.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +84,15 @@ impl XdgShellHandler for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
fn grab(&mut self, surface: PopupSurface, _seat: WlSeat, serial: Serial) {
|
||||||
|
// HACK: ignore grabs (pretend they work without actually grabbing) if the input method has
|
||||||
|
// a grab. It will likely need refactors in Smithay to support properly since grabs just
|
||||||
|
// replace each other.
|
||||||
|
// FIXME: do this properly.
|
||||||
|
if self.niri.seat.input_method().keyboard_grabbed() {
|
||||||
|
trace!("ignoring popup grab because IME has keyboard grabbed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let popup = PopupKind::Xdg(surface);
|
let popup = PopupKind::Xdg(surface);
|
||||||
let Ok(root) = find_popup_root_surface(&popup) else {
|
let Ok(root) = find_popup_root_surface(&popup) else {
|
||||||
return;
|
return;
|
||||||
@@ -185,9 +184,11 @@ impl XdgShellHandler for State {
|
|||||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
// FIXME
|
// FIXME
|
||||||
|
|
||||||
// The protocol demands us to always reply with a configure,
|
// A configure is required in response to this event. However, if an initial configure
|
||||||
// regardless of we fulfilled the request or not
|
// wasn't sent, then we will send this as part of the initial configure later.
|
||||||
surface.send_configure();
|
if initial_configure_sent(&surface) {
|
||||||
|
surface.send_configure();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
|
fn unmaximize_request(&mut self, _surface: ToplevelSurface) {
|
||||||
@@ -196,46 +197,167 @@ impl XdgShellHandler for State {
|
|||||||
|
|
||||||
fn fullscreen_request(
|
fn fullscreen_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
surface: ToplevelSurface,
|
toplevel: ToplevelSurface,
|
||||||
wl_output: Option<wl_output::WlOutput>,
|
wl_output: Option<wl_output::WlOutput>,
|
||||||
) {
|
) {
|
||||||
if surface
|
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
|
||||||
.current_state()
|
|
||||||
.capabilities
|
|
||||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
|
||||||
{
|
|
||||||
if let Some((window, current_output)) = self
|
|
||||||
.niri
|
|
||||||
.layout
|
|
||||||
.find_window_and_output(surface.wl_surface())
|
|
||||||
{
|
|
||||||
let window = window.clone();
|
|
||||||
|
|
||||||
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
|
if let Some((mapped, current_output)) = self
|
||||||
if &requested_output != current_output {
|
|
||||||
self.niri
|
|
||||||
.layout
|
|
||||||
.move_window_to_output(window.clone(), &requested_output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.niri.layout.set_fullscreen(&window, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The protocol demands us to always reply with a configure,
|
|
||||||
// regardless of we fulfilled the request or not
|
|
||||||
surface.send_configure();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
|
||||||
if let Some((window, _)) = self
|
|
||||||
.niri
|
.niri
|
||||||
.layout
|
.layout
|
||||||
.find_window_and_output(surface.wl_surface())
|
.find_window_and_output(toplevel.wl_surface())
|
||||||
{
|
{
|
||||||
let window = window.clone();
|
let window = mapped.window.clone();
|
||||||
|
|
||||||
|
if let Some(requested_output) = requested_output {
|
||||||
|
if &requested_output != current_output {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.move_window_to_output(&window, &requested_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } => {
|
||||||
|
*wants_fullscreen = Some(requested_output);
|
||||||
|
|
||||||
|
// The required configure will be the initial configure.
|
||||||
|
}
|
||||||
|
InitialConfigureState::Configured { output, .. } => {
|
||||||
|
// Figure out the monitor following a similar logic to initial configure.
|
||||||
|
// FIXME: deduplicate.
|
||||||
|
let mon = requested_output
|
||||||
|
.as_ref()
|
||||||
|
// If none requested, try currently configured output.
|
||||||
|
.or(output.as_ref())
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
|
.map(|mon| (mon, false))
|
||||||
|
// If not, check if we have a parent with a monitor.
|
||||||
|
.or_else(|| {
|
||||||
|
toplevel
|
||||||
|
.parent()
|
||||||
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
|
.map(|(_win, output)| output)
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
|
.map(|mon| (mon, true))
|
||||||
|
})
|
||||||
|
// If not, fall back to the active monitor.
|
||||||
|
.or_else(|| {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.active_monitor_ref()
|
||||||
|
.map(|mon| (mon, false))
|
||||||
|
});
|
||||||
|
|
||||||
|
*output = mon
|
||||||
|
.filter(|(_, parent)| !parent)
|
||||||
|
.map(|(mon, _)| mon.output.clone());
|
||||||
|
let mon = mon.map(|(mon, _)| mon);
|
||||||
|
|
||||||
|
let ws = mon
|
||||||
|
.map(|mon| mon.active_workspace_ref())
|
||||||
|
.or_else(|| self.niri.layout.active_workspace());
|
||||||
|
|
||||||
|
if let Some(ws) = ws {
|
||||||
|
toplevel.with_pending_state(|state| {
|
||||||
|
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||||
|
});
|
||||||
|
ws.configure_new_window(&unmapped.window, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We already sent the initial configure, so we need to reconfigure.
|
||||||
|
toplevel.send_configure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("couldn't find the toplevel in fullscreen_request()");
|
||||||
|
toplevel.send_configure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unfullscreen_request(&mut self, toplevel: ToplevelSurface) {
|
||||||
|
if let Some((mapped, _)) = self
|
||||||
|
.niri
|
||||||
|
.layout
|
||||||
|
.find_window_and_output(toplevel.wl_surface())
|
||||||
|
{
|
||||||
|
let window = mapped.window.clone();
|
||||||
self.niri.layout.set_fullscreen(&window, false);
|
self.niri.layout.set_fullscreen(&window, false);
|
||||||
|
|
||||||
|
// 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 } => {
|
||||||
|
*wants_fullscreen = None;
|
||||||
|
|
||||||
|
// The required configure will be the initial configure.
|
||||||
|
}
|
||||||
|
InitialConfigureState::Configured {
|
||||||
|
width,
|
||||||
|
is_full_width,
|
||||||
|
output,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// Figure out the monitor following a similar logic to initial configure.
|
||||||
|
// FIXME: deduplicate.
|
||||||
|
let mon = output
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
|
.map(|mon| (mon, false))
|
||||||
|
// If not, check if we have a parent with a monitor.
|
||||||
|
.or_else(|| {
|
||||||
|
toplevel
|
||||||
|
.parent()
|
||||||
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
|
.map(|(_win, output)| output)
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
|
.map(|mon| (mon, true))
|
||||||
|
})
|
||||||
|
// If not, fall back to the active monitor.
|
||||||
|
.or_else(|| {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.active_monitor_ref()
|
||||||
|
.map(|mon| (mon, false))
|
||||||
|
});
|
||||||
|
|
||||||
|
*output = mon
|
||||||
|
.filter(|(_, parent)| !parent)
|
||||||
|
.map(|(mon, _)| mon.output.clone());
|
||||||
|
let mon = mon.map(|(mon, _)| mon);
|
||||||
|
|
||||||
|
let ws = mon
|
||||||
|
.map(|mon| mon.active_workspace_ref())
|
||||||
|
.or_else(|| self.niri.layout.active_workspace());
|
||||||
|
|
||||||
|
if let Some(ws) = ws {
|
||||||
|
toplevel.with_pending_state(|state| {
|
||||||
|
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||||
|
});
|
||||||
|
|
||||||
|
let configure_width = if *is_full_width {
|
||||||
|
Some(ColumnWidth::Proportion(1.))
|
||||||
|
} else {
|
||||||
|
*width
|
||||||
|
};
|
||||||
|
ws.configure_new_window(&unmapped.window, configure_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We already sent the initial configure, so we need to reconfigure.
|
||||||
|
toplevel.send_configure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("couldn't find the toplevel in unfullscreen_request()");
|
||||||
|
toplevel.send_configure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,22 +377,49 @@ impl XdgShellHandler for State {
|
|||||||
.layout
|
.layout
|
||||||
.find_window_and_output(surface.wl_surface());
|
.find_window_and_output(surface.wl_surface());
|
||||||
|
|
||||||
let Some((window, output)) = win_out.map(clone2) else {
|
let Some((mapped, output)) = win_out else {
|
||||||
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
|
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
|
||||||
// involving laptop going to sleep and resuming.
|
// involving laptop going to sleep and resuming.
|
||||||
error!("toplevel missing from both unmapped_windows and layout");
|
error!("toplevel missing from both unmapped_windows and layout");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
let output = output.clone();
|
||||||
|
|
||||||
|
self.backend.with_primary_renderer(|renderer| {
|
||||||
|
mapped.store_unmap_snapshot_if_empty(renderer);
|
||||||
|
});
|
||||||
|
self.backend.with_primary_renderer(|renderer| {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.start_close_animation_for_window(renderer, &window);
|
||||||
|
});
|
||||||
|
|
||||||
|
let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
|
||||||
|
let was_active = active_window == Some(&window);
|
||||||
|
|
||||||
self.niri.layout.remove_window(&window);
|
self.niri.layout.remove_window(&window);
|
||||||
self.niri.queue_redraw(output);
|
|
||||||
|
if was_active {
|
||||||
|
self.maybe_warp_cursor_to_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
fn popup_destroyed(&mut self, surface: PopupSurface) {
|
||||||
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
|
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
|
||||||
self.niri.queue_redraw(output.clone());
|
self.niri.queue_redraw(&output.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn app_id_changed(&mut self, toplevel: ToplevelSurface) {
|
||||||
|
self.update_window_rules(&toplevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_changed(&mut self, toplevel: ToplevelSurface) {
|
||||||
|
self.update_window_rules(&toplevel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate_xdg_shell!(State);
|
delegate_xdg_shell!(State);
|
||||||
@@ -323,14 +472,14 @@ impl KdeDecorationHandler for State {
|
|||||||
&self.niri.kde_decoration_state
|
&self.niri.kde_decoration_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate_kde_decoration!(State);
|
delegate_kde_decoration!(State);
|
||||||
|
|
||||||
pub fn send_initial_configure_if_needed(toplevel: &ToplevelSurface) {
|
impl XdgForeignHandler for State {
|
||||||
if !initial_configure_sent(toplevel) {
|
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
|
||||||
toplevel.send_configure();
|
&mut self.niri.xdg_foreign_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delegate_xdg_foreign!(State);
|
||||||
|
|
||||||
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
||||||
with_states(toplevel.wl_surface(), |states| {
|
with_states(toplevel.wl_surface(), |states| {
|
||||||
@@ -345,6 +494,132 @@ fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
|
||||||
|
let _span = tracy_client::span!("State::send_initial_configure");
|
||||||
|
|
||||||
|
let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) else {
|
||||||
|
error!("window must be present in unmapped_windows in send_initial_configure()");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = self.niri.config.borrow();
|
||||||
|
let rules =
|
||||||
|
ResolvedWindowRules::compute(&config.window_rules, WindowRef::Unmapped(unmapped));
|
||||||
|
|
||||||
|
let Unmapped { window, state } = unmapped;
|
||||||
|
|
||||||
|
let InitialConfigureState::NotConfigured { wants_fullscreen } = state else {
|
||||||
|
error!("window must not be already configured in send_initial_configure()");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pick the target monitor. First, check if we had an output set in the window rules.
|
||||||
|
let mon = rules
|
||||||
|
.open_on_output
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|name| self.niri.output_by_name.get(name))
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o));
|
||||||
|
|
||||||
|
// If not, check if the window requested one for fullscreen.
|
||||||
|
let mon = mon.or_else(|| {
|
||||||
|
wants_fullscreen
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| x.as_ref())
|
||||||
|
// The monitor might not exist if the output was disconnected.
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
|
});
|
||||||
|
|
||||||
|
// If not, check if this is a dialog with a parent, to place it next to the parent.
|
||||||
|
let mon = mon.map(|mon| (mon, false)).or_else(|| {
|
||||||
|
toplevel
|
||||||
|
.parent()
|
||||||
|
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
|
||||||
|
.map(|(_win, output)| output)
|
||||||
|
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||||
|
.map(|mon| (mon, true))
|
||||||
|
});
|
||||||
|
|
||||||
|
// If not, use the active monitor.
|
||||||
|
let mon = mon.or_else(|| {
|
||||||
|
self.niri
|
||||||
|
.layout
|
||||||
|
.active_monitor_ref()
|
||||||
|
.map(|mon| (mon, false))
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're following the parent, don't set the target output, so that when the window is
|
||||||
|
// mapped, it fetches the possibly changed parent's output again, and shows up there.
|
||||||
|
let output = mon
|
||||||
|
.filter(|(_, parent)| !parent)
|
||||||
|
.map(|(mon, _)| mon.output.clone());
|
||||||
|
let mon = mon.map(|(mon, _)| mon);
|
||||||
|
|
||||||
|
let mut width = None;
|
||||||
|
let is_full_width = rules.open_maximized.unwrap_or(false);
|
||||||
|
|
||||||
|
// Tell the surface the preferred size and bounds for its likely output.
|
||||||
|
let ws = mon
|
||||||
|
.map(|mon| mon.active_workspace_ref())
|
||||||
|
.or_else(|| self.niri.layout.active_workspace());
|
||||||
|
|
||||||
|
if let Some(ws) = ws {
|
||||||
|
// Set a fullscreen state based on window request and window rule.
|
||||||
|
if (wants_fullscreen.is_some() && rules.open_fullscreen.is_none())
|
||||||
|
|| rules.open_fullscreen == Some(true)
|
||||||
|
{
|
||||||
|
toplevel.with_pending_state(|state| {
|
||||||
|
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
width = ws.resolve_default_width(rules.default_width);
|
||||||
|
|
||||||
|
let configure_width = if is_full_width {
|
||||||
|
Some(ColumnWidth::Proportion(1.))
|
||||||
|
} else {
|
||||||
|
width
|
||||||
|
};
|
||||||
|
ws.configure_new_window(window, configure_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 configured settings.
|
||||||
|
*state = InitialConfigureState::Configured {
|
||||||
|
rules,
|
||||||
|
width,
|
||||||
|
is_full_width,
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
|
toplevel.send_configure();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_initial_configure(&self, toplevel: ToplevelSurface) {
|
||||||
|
// Send the initial configure in an idle, in case the client sent some more info after the
|
||||||
|
// initial commit.
|
||||||
|
self.niri.event_loop.insert_idle(move |state| {
|
||||||
|
if !toplevel.alive() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(unmapped) = state.niri.unmapped_windows.get(toplevel.wl_surface()) {
|
||||||
|
if unmapped.needs_initial_configure() {
|
||||||
|
state.send_initial_configure(&toplevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Should be called on `WlSurface::commit`
|
/// Should be called on `WlSurface::commit`
|
||||||
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
|
pub fn popups_handle_commit(&mut self, surface: &WlSurface) {
|
||||||
self.niri.popups.commit(surface);
|
self.niri.popups.commit(surface);
|
||||||
@@ -394,8 +669,8 @@ impl State {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Figure out if the root is a window or a layer surface.
|
// Figure out if the root is a window or a layer surface.
|
||||||
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
|
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(&root) {
|
||||||
self.unconstrain_window_popup(popup, window, output);
|
self.unconstrain_window_popup(popup, &mapped.window, output);
|
||||||
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
|
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
|
||||||
let map = layer_map_for_output(o);
|
let map = layer_map_for_output(o);
|
||||||
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||||
@@ -451,7 +726,9 @@ impl State {
|
|||||||
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
|
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
|
||||||
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
let _span = tracy_client::span!("Niri::update_reactive_popups");
|
||||||
|
|
||||||
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
|
for (popup, _) in PopupManager::popups_for_surface(
|
||||||
|
window.toplevel().expect("no x11 support").wl_surface(),
|
||||||
|
) {
|
||||||
match popup {
|
match popup {
|
||||||
PopupKind::Xdg(ref popup) => {
|
PopupKind::Xdg(ref popup) => {
|
||||||
if popup.with_pending_state(|state| state.positioner.reactive) {
|
if popup.with_pending_state(|state| state.positioner.reactive) {
|
||||||
@@ -465,6 +742,34 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_window_rules(&mut self, toplevel: &ToplevelSurface) {
|
||||||
|
let config = self.niri.config.borrow();
|
||||||
|
let window_rules = &config.window_rules;
|
||||||
|
|
||||||
|
if let Some(unmapped) = self.niri.unmapped_windows.get_mut(toplevel.wl_surface()) {
|
||||||
|
let new_rules =
|
||||||
|
ResolvedWindowRules::compute(window_rules, WindowRef::Unmapped(unmapped));
|
||||||
|
if let InitialConfigureState::Configured { rules, .. } = &mut unmapped.state {
|
||||||
|
*rules = new_rules;
|
||||||
|
}
|
||||||
|
} else if let Some((mapped, output)) = self
|
||||||
|
.niri
|
||||||
|
.layout
|
||||||
|
.find_window_and_output_mut(toplevel.wl_surface())
|
||||||
|
{
|
||||||
|
if mapped.recompute_window_rules(window_rules) {
|
||||||
|
drop(config);
|
||||||
|
let output = output.cloned();
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
self.niri.layout.update_window(&window);
|
||||||
|
|
||||||
|
if let Some(output) = output {
|
||||||
|
self.niri.queue_redraw(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unconstrain_with_padding(
|
fn unconstrain_with_padding(
|
||||||
@@ -507,3 +812,53 @@ fn unconstrain_with_padding(
|
|||||||
// Could not unconstrain into the padded target, so resort to the regular one.
|
// Could not unconstrain into the padded target, so resort to the regular one.
|
||||||
positioner.get_unconstrained_geometry(target)
|
positioner.get_unconstrained_geometry(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId {
|
||||||
|
add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| {
|
||||||
|
let _span = tracy_client::span!("mapped toplevel pre-commit");
|
||||||
|
|
||||||
|
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
|
||||||
|
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (got_unmapped, commit_serial) = with_states(surface, |states| {
|
||||||
|
let attrs = states.cached_state.pending::<SurfaceAttributes>();
|
||||||
|
let got_unmapped = matches!(attrs.buffer, Some(BufferAssignment::Removed));
|
||||||
|
|
||||||
|
let role = states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(got_unmapped, role.configure_serial)
|
||||||
|
});
|
||||||
|
|
||||||
|
let animate = if let Some(serial) = commit_serial {
|
||||||
|
mapped.should_animate_commit(serial)
|
||||||
|
} else {
|
||||||
|
error!("commit on a mapped surface without a configured serial");
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if got_unmapped {
|
||||||
|
state.backend.with_primary_renderer(|renderer| {
|
||||||
|
mapped.store_unmap_snapshot_if_empty(renderer);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// The toplevel remains mapped; clear any stored unmap snapshot.
|
||||||
|
let _ = mapped.take_unmap_snapshot();
|
||||||
|
|
||||||
|
if animate {
|
||||||
|
state.backend.with_primary_renderer(|renderer| {
|
||||||
|
mapped.store_animation_snapshot(renderer);
|
||||||
|
});
|
||||||
|
|
||||||
|
let window = mapped.window.clone();
|
||||||
|
state.niri.layout.prepare_for_resize_animation(&window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+968
-142
File diff suppressed because it is too large
Load Diff
+176
-39
@@ -1,46 +1,101 @@
|
|||||||
use std::env;
|
use anyhow::{anyhow, bail, Context};
|
||||||
use std::io::{Read, Write};
|
use niri_ipc::{LogicalOutput, Mode, Output, Request, Response, Socket, Transform};
|
||||||
use std::net::Shutdown;
|
use serde_json::json;
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use crate::cli::Msg;
|
||||||
use niri_ipc::{Mode, Output, Request, Response};
|
use crate::utils::version;
|
||||||
|
|
||||||
use crate::Msg;
|
|
||||||
|
|
||||||
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||||
let socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
|
let request = match &msg {
|
||||||
format!(
|
Msg::Version => Request::Version,
|
||||||
"{} is not set, are you running this within niri?",
|
Msg::Outputs => Request::Outputs,
|
||||||
niri_ipc::SOCKET_PATH_ENV
|
Msg::FocusedWindow => Request::FocusedWindow,
|
||||||
)
|
Msg::Action { action } => Request::Action(action.clone()),
|
||||||
|
Msg::RequestError => Request::ReturnError,
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||||
|
|
||||||
|
let reply = socket
|
||||||
|
.send(request)
|
||||||
|
.context("error communicating with niri")?;
|
||||||
|
|
||||||
|
let compositor_version = match reply {
|
||||||
|
Err(_) if !matches!(msg, Msg::Version) => {
|
||||||
|
// If we got an error, it might be that the CLI is a different version from the running
|
||||||
|
// niri instance. Request the running instance version to compare and print a message.
|
||||||
|
Socket::connect()
|
||||||
|
.and_then(|socket| socket.send(Request::Version))
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default SIGPIPE so that our prints don't panic on stdout closing.
|
||||||
|
unsafe {
|
||||||
|
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = reply.map_err(|err_msg| {
|
||||||
|
// Check for CLI-server version mismatch to add helpful context.
|
||||||
|
match compositor_version {
|
||||||
|
Some(Ok(Response::Version(compositor_version))) => {
|
||||||
|
let cli_version = version();
|
||||||
|
if cli_version != compositor_version {
|
||||||
|
eprintln!("Running niri compositor has a different version from the niri CLI:");
|
||||||
|
eprintln!("Compositor version: {compositor_version}");
|
||||||
|
eprintln!("CLI version: {cli_version}");
|
||||||
|
eprintln!("Did you forget to restart niri after an update?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
eprintln!("Unable to get the running niri compositor version.");
|
||||||
|
eprintln!("Did you forget to restart niri after an update?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Communication error, or the original request was already a version request.
|
||||||
|
// Don't add irrelevant context.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow!(err_msg).context("niri returned an error")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut stream =
|
|
||||||
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
|
||||||
|
|
||||||
let request = match msg {
|
|
||||||
Msg::Outputs => Request::Outputs,
|
|
||||||
};
|
|
||||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
|
||||||
stream
|
|
||||||
.write_all(&buf)
|
|
||||||
.context("error writing IPC request")?;
|
|
||||||
stream
|
|
||||||
.shutdown(Shutdown::Write)
|
|
||||||
.context("error closing IPC stream for writing")?;
|
|
||||||
|
|
||||||
buf.clear();
|
|
||||||
stream
|
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.context("error reading IPC response")?;
|
|
||||||
|
|
||||||
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
|
|
||||||
match msg {
|
match msg {
|
||||||
|
Msg::RequestError => {
|
||||||
|
bail!("unexpected response: expected an error, got {response:?}");
|
||||||
|
}
|
||||||
|
Msg::Version => {
|
||||||
|
let Response::Version(compositor_version) = response else {
|
||||||
|
bail!("unexpected response: expected Version, got {response:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let cli_version = version();
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
json!({
|
||||||
|
"compositor": compositor_version,
|
||||||
|
"cli": cli_version,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli_version != compositor_version {
|
||||||
|
eprintln!("Running niri compositor has a different version from the niri CLI.");
|
||||||
|
eprintln!("Did you forget to restart niri after an update?");
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Compositor version: {compositor_version}");
|
||||||
|
println!("CLI version: {cli_version}");
|
||||||
|
}
|
||||||
Msg::Outputs => {
|
Msg::Outputs => {
|
||||||
#[allow(irrefutable_let_patterns)]
|
let Response::Outputs(outputs) = response else {
|
||||||
let Response::Outputs(outputs) = response
|
|
||||||
else {
|
|
||||||
bail!("unexpected response: expected Outputs, got {response:?}");
|
bail!("unexpected response: expected Outputs, got {response:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,6 +117,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
physical_size,
|
physical_size,
|
||||||
modes,
|
modes,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
vrr_supported,
|
||||||
|
vrr_enabled,
|
||||||
|
logical,
|
||||||
} = output;
|
} = output;
|
||||||
|
|
||||||
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
||||||
@@ -74,32 +132,111 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
refresh_rate,
|
refresh_rate,
|
||||||
|
is_preferred,
|
||||||
} = mode;
|
} = mode;
|
||||||
let refresh = refresh_rate as f64 / 1000.;
|
let refresh = refresh_rate as f64 / 1000.;
|
||||||
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz");
|
let preferred = if is_preferred { " (preferred)" } else { "" };
|
||||||
|
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
|
||||||
} else {
|
} else {
|
||||||
println!(" Disabled");
|
println!(" Disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vrr_supported {
|
||||||
|
let enabled = if vrr_enabled { "enabled" } else { "disabled" };
|
||||||
|
println!(" Variable refresh rate: supported, {enabled}");
|
||||||
|
} else {
|
||||||
|
println!(" Variable refresh rate: not supported");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((width, height)) = physical_size {
|
if let Some((width, height)) = physical_size {
|
||||||
println!(" Physical size: {width}x{height} mm");
|
println!(" Physical size: {width}x{height} mm");
|
||||||
} else {
|
} else {
|
||||||
println!(" Physical size: unknown");
|
println!(" Physical size: unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(logical) = logical {
|
||||||
|
let LogicalOutput {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
scale,
|
||||||
|
transform,
|
||||||
|
} = logical;
|
||||||
|
println!(" Logical position: {x}, {y}");
|
||||||
|
println!(" Logical size: {width}x{height}");
|
||||||
|
println!(" Scale: {scale}");
|
||||||
|
|
||||||
|
let transform = match transform {
|
||||||
|
Transform::Normal => "normal",
|
||||||
|
Transform::_90 => "90° counter-clockwise",
|
||||||
|
Transform::_180 => "180°",
|
||||||
|
Transform::_270 => "270° counter-clockwise",
|
||||||
|
Transform::Flipped => "flipped horizontally",
|
||||||
|
Transform::Flipped90 => "90° counter-clockwise, flipped horizontally",
|
||||||
|
Transform::Flipped180 => "flipped vertically",
|
||||||
|
Transform::Flipped270 => "270° counter-clockwise, flipped horizontally",
|
||||||
|
};
|
||||||
|
println!(" Transform: {transform}");
|
||||||
|
}
|
||||||
|
|
||||||
println!(" Available modes:");
|
println!(" Available modes:");
|
||||||
for mode in modes {
|
for (idx, mode) in modes.into_iter().enumerate() {
|
||||||
let Mode {
|
let Mode {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
refresh_rate,
|
refresh_rate,
|
||||||
|
is_preferred,
|
||||||
} = mode;
|
} = mode;
|
||||||
let refresh = refresh_rate as f64 / 1000.;
|
let refresh = refresh_rate as f64 / 1000.;
|
||||||
println!(" {width}x{height}@{refresh:.3}");
|
|
||||||
|
let is_current = Some(idx) == current_mode;
|
||||||
|
let qualifier = match (is_current, is_preferred) {
|
||||||
|
(true, true) => " (current, preferred)",
|
||||||
|
(true, false) => " (current)",
|
||||||
|
(false, true) => " (preferred)",
|
||||||
|
(false, false) => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(" {width}x{height}@{refresh:.3}{qualifier}");
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Msg::FocusedWindow => {
|
||||||
|
let Response::FocusedWindow(window) = response else {
|
||||||
|
bail!("unexpected response: expected FocusedWindow, got {response:?}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let window = serde_json::to_string(&window).context("error formatting response")?;
|
||||||
|
println!("{window}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = window {
|
||||||
|
println!("Focused window:");
|
||||||
|
|
||||||
|
if let Some(title) = window.title {
|
||||||
|
println!(" Title: \"{title}\"");
|
||||||
|
} else {
|
||||||
|
println!(" Title: (unset)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(app_id) = window.app_id {
|
||||||
|
println!(" App ID: \"{app_id}\"");
|
||||||
|
} else {
|
||||||
|
println!(" App ID: (unset)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("No window is focused.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::Action { .. } => {
|
||||||
|
let Response::Handled = response else {
|
||||||
|
bail!("unexpected response: expected Handled, got {response:?}");
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+64
-16
@@ -1,8 +1,6 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::{env, io, process};
|
use std::{env, io, process};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
@@ -10,19 +8,26 @@ use calloop::io::Async;
|
|||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use futures_util::io::{AsyncReadExt, BufReader};
|
use futures_util::io::{AsyncReadExt, BufReader};
|
||||||
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
|
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
|
||||||
use niri_ipc::{Request, Response};
|
use niri_ipc::{Reply, Request, Response};
|
||||||
|
use smithay::desktop::Window;
|
||||||
use smithay::reexports::calloop::generic::Generic;
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||||
use smithay::reexports::rustix::fs::unlink;
|
use smithay::reexports::rustix::fs::unlink;
|
||||||
|
use smithay::wayland::compositor::with_states;
|
||||||
|
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||||
|
|
||||||
|
use crate::backend::IpcOutputMap;
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
|
use crate::utils::version;
|
||||||
|
|
||||||
pub struct IpcServer {
|
pub struct IpcServer {
|
||||||
pub socket_path: PathBuf,
|
pub socket_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ClientCtx {
|
struct ClientCtx {
|
||||||
ipc_outputs: Rc<RefCell<HashMap<String, niri_ipc::Output>>>,
|
event_loop: LoopHandle<'static, State>,
|
||||||
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||||
|
ipc_focused_window: Arc<Mutex<Option<Window>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IpcServer {
|
impl IpcServer {
|
||||||
@@ -85,7 +90,9 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let ctx = ClientCtx {
|
let ctx = ClientCtx {
|
||||||
|
event_loop: state.niri.event_loop.clone(),
|
||||||
ipc_outputs: state.backend.ipc_outputs(),
|
ipc_outputs: state.backend.ipc_outputs(),
|
||||||
|
ipc_focused_window: state.niri.ipc_focused_window.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let future = async move {
|
let future = async move {
|
||||||
@@ -108,20 +115,61 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
|
|||||||
.await
|
.await
|
||||||
.context("error reading request")?;
|
.context("error reading request")?;
|
||||||
|
|
||||||
let request: Request = serde_json::from_str(&buf).context("error parsing request")?;
|
let request = serde_json::from_str(&buf)
|
||||||
|
.context("error parsing request")
|
||||||
|
.map_err(|err| err.to_string());
|
||||||
|
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||||
|
|
||||||
let response = match request {
|
let reply = request.and_then(|request| process(&ctx, request));
|
||||||
Request::Outputs => {
|
|
||||||
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
|
if let Err(err) = &reply {
|
||||||
Response::Outputs(ipc_outputs)
|
if !requested_error {
|
||||||
|
warn!("error processing IPC request: {err:?}");
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let buf = serde_json::to_vec(&response).context("error formatting response")?;
|
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||||
write
|
write.write_all(&buf).await.context("error writing reply")?;
|
||||||
.write_all(&buf)
|
|
||||||
.await
|
|
||||||
.context("error writing response")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||||
|
let response = match request {
|
||||||
|
Request::ReturnError => return Err(String::from("example compositor error")),
|
||||||
|
Request::Version => Response::Version(version()),
|
||||||
|
Request::Outputs => {
|
||||||
|
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
|
||||||
|
Response::Outputs(ipc_outputs)
|
||||||
|
}
|
||||||
|
Request::FocusedWindow => {
|
||||||
|
let window = ctx.ipc_focused_window.lock().unwrap().clone();
|
||||||
|
let window = window.map(|window| {
|
||||||
|
let wl_surface = window.toplevel().expect("no X11 support").wl_surface();
|
||||||
|
with_states(wl_surface, |states| {
|
||||||
|
let role = states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
niri_ipc::Window {
|
||||||
|
title: role.title.clone(),
|
||||||
|
app_id: role.app_id.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Response::FocusedWindow(window)
|
||||||
|
}
|
||||||
|
Request::Action(action) => {
|
||||||
|
let action = niri_config::Action::from(action);
|
||||||
|
ctx.event_loop.insert_idle(move |state| {
|
||||||
|
state.do_action(action, false);
|
||||||
|
});
|
||||||
|
Response::Handled
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use niri_config::BlockOutFrom;
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||||
|
use smithay::backend::renderer::element::utils::{
|
||||||
|
Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||||
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
|
use smithay::utils::{Logical, Point, Scale, Transform};
|
||||||
|
|
||||||
|
use crate::animation::Animation;
|
||||||
|
use crate::niri_render_elements;
|
||||||
|
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||||
|
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||||
|
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ClosingWindow {
|
||||||
|
/// Contents of the window.
|
||||||
|
buffer: TextureBuffer<GlesTexture>,
|
||||||
|
|
||||||
|
/// Blocked-out contents of the window.
|
||||||
|
blocked_out_buffer: TextureBuffer<GlesTexture>,
|
||||||
|
|
||||||
|
/// Where the window should be blocked out from.
|
||||||
|
block_out_from: Option<BlockOutFrom>,
|
||||||
|
|
||||||
|
/// Center of the window geometry.
|
||||||
|
center: Point<i32, Logical>,
|
||||||
|
|
||||||
|
/// Position in the workspace.
|
||||||
|
pos: Point<i32, Logical>,
|
||||||
|
|
||||||
|
/// How much the buffer should be offset.
|
||||||
|
buffer_offset: Point<i32, Logical>,
|
||||||
|
|
||||||
|
/// How much the blocked-out buffer should be offset.
|
||||||
|
blocked_out_buffer_offset: Point<i32, Logical>,
|
||||||
|
|
||||||
|
/// The closing animation.
|
||||||
|
anim: Animation,
|
||||||
|
|
||||||
|
/// Alpha the animation should start from.
|
||||||
|
starting_alpha: f32,
|
||||||
|
|
||||||
|
/// Scale the animation should start from.
|
||||||
|
starting_scale: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
ClosingWindowRenderElement => {
|
||||||
|
Texture = RelocateRenderElement<RescaleRenderElement<PrimaryGpuTextureRenderElement>>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClosingWindow {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new<E: RenderElement<GlesRenderer>>(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
snapshot: RenderSnapshot<E, E>,
|
||||||
|
scale: i32,
|
||||||
|
center: Point<i32, Logical>,
|
||||||
|
pos: Point<i32, Logical>,
|
||||||
|
anim: Animation,
|
||||||
|
starting_alpha: f32,
|
||||||
|
starting_scale: f64,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let _span = tracy_client::span!("ClosingWindow::new");
|
||||||
|
|
||||||
|
let mut render_to_buffer = |elements: Vec<E>| -> anyhow::Result<_> {
|
||||||
|
let (texture, _sync_point, geo) = render_to_encompassing_texture(
|
||||||
|
renderer,
|
||||||
|
Scale::from(scale as f64),
|
||||||
|
Transform::Normal,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
&elements,
|
||||||
|
)
|
||||||
|
.context("error rendering to texture")?;
|
||||||
|
|
||||||
|
let buffer =
|
||||||
|
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None);
|
||||||
|
let offset = geo.loc.to_logical(scale);
|
||||||
|
|
||||||
|
Ok((buffer, offset))
|
||||||
|
};
|
||||||
|
|
||||||
|
let (buffer, buffer_offset) =
|
||||||
|
render_to_buffer(snapshot.contents).context("error rendering contents")?;
|
||||||
|
let (blocked_out_buffer, blocked_out_buffer_offset) =
|
||||||
|
render_to_buffer(snapshot.blocked_out_contents)
|
||||||
|
.context("error rendering blocked-out contents")?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
buffer,
|
||||||
|
blocked_out_buffer,
|
||||||
|
block_out_from: snapshot.block_out_from,
|
||||||
|
center,
|
||||||
|
pos,
|
||||||
|
buffer_offset,
|
||||||
|
blocked_out_buffer_offset,
|
||||||
|
anim,
|
||||||
|
starting_alpha,
|
||||||
|
starting_scale,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||||
|
self.anim.set_current_time(current_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
!self.anim.is_clamped_done()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
view_pos: i32,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> ClosingWindowRenderElement {
|
||||||
|
let val = self.anim.clamped_value();
|
||||||
|
|
||||||
|
let block_out = match self.block_out_from {
|
||||||
|
None => false,
|
||||||
|
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
|
||||||
|
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
|
||||||
|
};
|
||||||
|
let (buffer, offset) = if block_out {
|
||||||
|
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
|
||||||
|
} else {
|
||||||
|
(&self.buffer, self.buffer_offset)
|
||||||
|
};
|
||||||
|
|
||||||
|
let elem = TextureRenderElement::from_texture_buffer(
|
||||||
|
Point::from((0., 0.)),
|
||||||
|
buffer,
|
||||||
|
Some(val.clamp(0., 1.) as f32 * self.starting_alpha),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||||
|
|
||||||
|
let elem = RescaleRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
(self.center - offset).to_physical_precise_round(scale),
|
||||||
|
((val / 5. + 0.8) * self.starting_scale).max(0.),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut location = self.pos + offset;
|
||||||
|
location.x -= view_pos;
|
||||||
|
let elem = RelocateRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
Relocate::Relative,
|
||||||
|
);
|
||||||
|
|
||||||
|
elem.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
-50
@@ -1,64 +1,72 @@
|
|||||||
use std::iter::zip;
|
use std::iter::zip;
|
||||||
|
|
||||||
use arrayvec::ArrayVec;
|
use arrayvec::ArrayVec;
|
||||||
use niri_config::{self, Color};
|
use niri_config::GradientRelativeTo;
|
||||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use smithay::backend::renderer::element::Kind;
|
use smithay::backend::renderer::element::Kind;
|
||||||
use smithay::utils::{Logical, Point, Scale, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||||
|
|
||||||
|
use crate::niri_render_elements;
|
||||||
|
use crate::render_helpers::gradient::GradientRenderElement;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FocusRing {
|
pub struct FocusRing {
|
||||||
buffers: [SolidColorBuffer; 4],
|
buffers: [SolidColorBuffer; 4],
|
||||||
locations: [Point<i32, Logical>; 4],
|
locations: [Point<i32, Logical>; 4],
|
||||||
is_off: bool,
|
sizes: [Size<i32, Logical>; 4],
|
||||||
|
full_size: Size<i32, Logical>,
|
||||||
|
is_active: bool,
|
||||||
is_border: bool,
|
is_border: bool,
|
||||||
width: i32,
|
config: niri_config::FocusRing,
|
||||||
active_color: Color,
|
|
||||||
inactive_color: Color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FocusRingRenderElement = SolidColorRenderElement;
|
niri_render_elements! {
|
||||||
|
FocusRingRenderElement => {
|
||||||
|
SolidColor = SolidColorRenderElement,
|
||||||
|
Gradient = GradientRenderElement,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FocusRing {
|
impl FocusRing {
|
||||||
pub fn new(config: niri_config::FocusRing) -> Self {
|
pub fn new(config: niri_config::FocusRing) -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffers: Default::default(),
|
buffers: Default::default(),
|
||||||
locations: Default::default(),
|
locations: Default::default(),
|
||||||
is_off: config.off,
|
sizes: Default::default(),
|
||||||
|
full_size: Default::default(),
|
||||||
|
is_active: false,
|
||||||
is_border: false,
|
is_border: false,
|
||||||
width: config.width.into(),
|
config,
|
||||||
active_color: config.active_color,
|
|
||||||
inactive_color: config.inactive_color,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(&mut self, config: niri_config::FocusRing) {
|
pub fn update_config(&mut self, config: niri_config::FocusRing) {
|
||||||
self.is_off = config.off;
|
self.config = config;
|
||||||
self.width = config.width.into();
|
|
||||||
self.active_color = config.active_color;
|
|
||||||
self.inactive_color = config.inactive_color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(&mut self, win_size: Size<i32, Logical>, is_border: bool) {
|
||||||
&mut self,
|
let width = i32::from(self.config.width);
|
||||||
win_pos: Point<i32, Logical>,
|
self.full_size = win_size + Size::from((width * 2, width * 2));
|
||||||
win_size: Size<i32, Logical>,
|
|
||||||
is_border: bool,
|
|
||||||
) {
|
|
||||||
if is_border {
|
|
||||||
self.buffers[0].resize((win_size.w + self.width * 2, self.width));
|
|
||||||
self.buffers[1].resize((win_size.w + self.width * 2, self.width));
|
|
||||||
self.buffers[2].resize((self.width, win_size.h));
|
|
||||||
self.buffers[3].resize((self.width, win_size.h));
|
|
||||||
|
|
||||||
self.locations[0] = win_pos + Point::from((-self.width, -self.width));
|
if is_border {
|
||||||
self.locations[1] = win_pos + Point::from((-self.width, win_size.h));
|
self.sizes[0] = Size::from((win_size.w + width * 2, width));
|
||||||
self.locations[2] = win_pos + Point::from((-self.width, 0));
|
self.sizes[1] = Size::from((win_size.w + width * 2, width));
|
||||||
self.locations[3] = win_pos + Point::from((win_size.w, 0));
|
self.sizes[2] = Size::from((width, win_size.h));
|
||||||
|
self.sizes[3] = Size::from((width, win_size.h));
|
||||||
|
|
||||||
|
for (buf, size) in zip(&mut self.buffers, self.sizes) {
|
||||||
|
buf.resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.locations[0] = Point::from((-width, -width));
|
||||||
|
self.locations[1] = Point::from((-width, win_size.h));
|
||||||
|
self.locations[2] = Point::from((-width, 0));
|
||||||
|
self.locations[3] = Point::from((win_size.w, 0));
|
||||||
} else {
|
} else {
|
||||||
let size = win_size + Size::from((self.width * 2, self.width * 2));
|
self.sizes[0] = self.full_size;
|
||||||
self.buffers[0].resize(size);
|
self.buffers[0].resize(self.sizes[0]);
|
||||||
self.locations[0] = win_pos - Point::from((self.width, self.width));
|
self.locations[0] = Point::from((-width, -width));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.is_border = is_border;
|
self.is_border = is_border;
|
||||||
@@ -66,50 +74,91 @@ impl FocusRing {
|
|||||||
|
|
||||||
pub fn set_active(&mut self, is_active: bool) {
|
pub fn set_active(&mut self, is_active: bool) {
|
||||||
let color = if is_active {
|
let color = if is_active {
|
||||||
self.active_color.into()
|
self.config.active_color.into()
|
||||||
} else {
|
} else {
|
||||||
self.inactive_color.into()
|
self.config.inactive_color.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
for buf in &mut self.buffers {
|
for buf in &mut self.buffers {
|
||||||
buf.set_color(color);
|
buf.set_color(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.is_active = is_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self, scale: Scale<f64>) -> impl Iterator<Item = FocusRingRenderElement> {
|
pub fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
view_size: Size<i32, Logical>,
|
||||||
|
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||||
let mut rv = ArrayVec::<_, 4>::new();
|
let mut rv = ArrayVec::<_, 4>::new();
|
||||||
|
|
||||||
if self.is_off {
|
if self.config.off {
|
||||||
return rv.into_iter();
|
return rv.into_iter();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut push = |buffer, location: Point<i32, Logical>| {
|
let gradient = if self.is_active {
|
||||||
let elem = SolidColorRenderElement::from_buffer(
|
self.config.active_gradient
|
||||||
buffer,
|
} else {
|
||||||
location.to_physical_precise_round(scale),
|
self.config.inactive_gradient
|
||||||
scale,
|
};
|
||||||
1.,
|
|
||||||
Kind::Unspecified,
|
let full_rect = Rectangle::from_loc_and_size(location + self.locations[0], self.full_size);
|
||||||
);
|
let view_rect = Rectangle::from_loc_and_size((0, 0), view_size);
|
||||||
|
|
||||||
|
let mut push = |buffer, location: Point<i32, Logical>, size: Size<i32, Logical>| {
|
||||||
|
let elem = gradient.and_then(|gradient| {
|
||||||
|
let gradient_area = match gradient.relative_to {
|
||||||
|
GradientRelativeTo::Window => full_rect,
|
||||||
|
GradientRelativeTo::WorkspaceView => view_rect,
|
||||||
|
};
|
||||||
|
GradientRenderElement::new(
|
||||||
|
renderer,
|
||||||
|
scale,
|
||||||
|
Rectangle::from_loc_and_size(location, size),
|
||||||
|
gradient_area,
|
||||||
|
gradient.from.into(),
|
||||||
|
gradient.to.into(),
|
||||||
|
((gradient.angle as f32) - 90.).to_radians(),
|
||||||
|
)
|
||||||
|
.map(Into::into)
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem = elem.unwrap_or_else(|| {
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
buffer,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
1.,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
});
|
||||||
rv.push(elem);
|
rv.push(elem);
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.is_border {
|
if self.is_border {
|
||||||
for (buf, loc) in zip(&self.buffers, self.locations) {
|
for (buf, (loc, size)) in zip(&self.buffers, zip(self.locations, self.sizes)) {
|
||||||
push(buf, loc);
|
push(buf, location + loc, size);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
push(&self.buffers[0], self.locations[0]);
|
push(
|
||||||
|
&self.buffers[0],
|
||||||
|
location + self.locations[0],
|
||||||
|
self.sizes[0],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
rv.into_iter()
|
rv.into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn width(&self) -> i32 {
|
pub fn width(&self) -> i32 {
|
||||||
self.width
|
self.config.width.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_off(&self) -> bool {
|
pub fn is_off(&self) -> bool {
|
||||||
self.is_off
|
self.config.off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+971
-231
File diff suppressed because it is too large
Load Diff
+272
-42
@@ -2,22 +2,33 @@ use std::cmp::min;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use niri_config::SizeChange;
|
use niri_ipc::SizeChange;
|
||||||
use smithay::backend::renderer::element::utils::{
|
use smithay::backend::renderer::element::utils::{
|
||||||
CropRenderElement, Relocate, RelocateRenderElement,
|
CropRenderElement, Relocate, RelocateRenderElement,
|
||||||
};
|
};
|
||||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
|
||||||
use smithay::desktop::Window;
|
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
use smithay::utils::{Logical, Point, Rectangle, Scale};
|
||||||
|
|
||||||
use super::workspace::{
|
use super::workspace::{
|
||||||
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceRenderElement,
|
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
|
||||||
|
WorkspaceRenderElement,
|
||||||
};
|
};
|
||||||
use super::{LayoutElement, Options};
|
use super::{LayoutElement, Options};
|
||||||
use crate::animation::Animation;
|
use crate::animation::Animation;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::RenderTarget;
|
||||||
|
use crate::rubber_band::RubberBand;
|
||||||
|
use crate::swipe_tracker::SwipeTracker;
|
||||||
use crate::utils::output_size;
|
use crate::utils::output_size;
|
||||||
|
|
||||||
|
/// Amount of touchpad movement to scroll the height of one workspace.
|
||||||
|
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
|
||||||
|
|
||||||
|
const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
|
||||||
|
stiffness: 0.5,
|
||||||
|
limit: 0.05,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Monitor<W: LayoutElement> {
|
pub struct Monitor<W: LayoutElement> {
|
||||||
/// Output for this monitor.
|
/// Output for this monitor.
|
||||||
@@ -26,6 +37,8 @@ pub struct Monitor<W: LayoutElement> {
|
|||||||
pub workspaces: Vec<Workspace<W>>,
|
pub workspaces: Vec<Workspace<W>>,
|
||||||
/// Index of the currently active workspace.
|
/// Index of the currently active workspace.
|
||||||
pub active_workspace_idx: usize,
|
pub active_workspace_idx: usize,
|
||||||
|
/// ID of the previously active workspace.
|
||||||
|
pub previous_workspace_id: Option<WorkspaceId>,
|
||||||
/// In-progress switch between workspaces.
|
/// In-progress switch between workspaces.
|
||||||
pub workspace_switch: Option<WorkspaceSwitch>,
|
pub workspace_switch: Option<WorkspaceSwitch>,
|
||||||
/// Configurable properties of the layout.
|
/// Configurable properties of the layout.
|
||||||
@@ -44,6 +57,7 @@ pub struct WorkspaceSwitchGesture {
|
|||||||
pub center_idx: usize,
|
pub center_idx: usize,
|
||||||
/// Current, fractional workspace index.
|
/// Current, fractional workspace index.
|
||||||
pub current_idx: f64,
|
pub current_idx: f64,
|
||||||
|
pub tracker: SwipeTracker,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MonitorRenderElement<R> =
|
pub type MonitorRenderElement<R> =
|
||||||
@@ -57,6 +71,13 @@ impl WorkspaceSwitch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn target_idx(&self) -> f64 {
|
||||||
|
match self {
|
||||||
|
WorkspaceSwitch::Animation(anim) => anim.to(),
|
||||||
|
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if the workspace switch is [`Animation`].
|
/// Returns `true` if the workspace switch is [`Animation`].
|
||||||
///
|
///
|
||||||
/// [`Animation`]: WorkspaceSwitch::Animation
|
/// [`Animation`]: WorkspaceSwitch::Animation
|
||||||
@@ -72,11 +93,16 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
output,
|
output,
|
||||||
workspaces,
|
workspaces,
|
||||||
active_workspace_idx: 0,
|
active_workspace_idx: 0,
|
||||||
|
previous_workspace_id: None,
|
||||||
workspace_switch: None,
|
workspace_switch: None,
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_workspace_ref(&self) -> &Workspace<W> {
|
||||||
|
&self.workspaces[self.active_workspace_idx]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
|
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
|
||||||
&mut self.workspaces[self.active_workspace_idx]
|
&mut self.workspaces[self.active_workspace_idx]
|
||||||
}
|
}
|
||||||
@@ -86,18 +112,22 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: also compute and use current velocity.
|
||||||
let current_idx = self
|
let current_idx = self
|
||||||
.workspace_switch
|
.workspace_switch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.current_idx())
|
.map(|s| s.current_idx())
|
||||||
.unwrap_or(self.active_workspace_idx as f64);
|
.unwrap_or(self.active_workspace_idx as f64);
|
||||||
|
|
||||||
|
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
|
||||||
|
|
||||||
self.active_workspace_idx = idx;
|
self.active_workspace_idx = idx;
|
||||||
|
|
||||||
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||||
current_idx,
|
current_idx,
|
||||||
idx as f64,
|
idx as f64,
|
||||||
Duration::from_millis(250),
|
0.,
|
||||||
|
self.options.animations.workspace_switch.0,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +157,26 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_window_right_of(
|
||||||
|
&mut self,
|
||||||
|
right_of: &W::Id,
|
||||||
|
window: W,
|
||||||
|
width: ColumnWidth,
|
||||||
|
is_full_width: bool,
|
||||||
|
) {
|
||||||
|
let workspace_idx = self
|
||||||
|
.workspaces
|
||||||
|
.iter_mut()
|
||||||
|
.position(|ws| ws.has_window(right_of))
|
||||||
|
.unwrap();
|
||||||
|
let workspace = &mut self.workspaces[workspace_idx];
|
||||||
|
|
||||||
|
workspace.add_window_right_of(right_of, window, width, is_full_width);
|
||||||
|
|
||||||
|
// After adding a new window, workspace becomes this output's own.
|
||||||
|
workspace.original_output = OutputId::new(&self.output);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
||||||
let workspace = &mut self.workspaces[workspace_idx];
|
let workspace = &mut self.workspaces[workspace_idx];
|
||||||
|
|
||||||
@@ -216,6 +266,14 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn consume_or_expel_window_left(&mut self) {
|
||||||
|
self.active_workspace().consume_or_expel_window_left();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_or_expel_window_right(&mut self) {
|
||||||
|
self.active_workspace().consume_or_expel_window_right();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn focus_left(&mut self) {
|
pub fn focus_left(&mut self) {
|
||||||
self.active_workspace().focus_left();
|
self.active_workspace().focus_left();
|
||||||
}
|
}
|
||||||
@@ -287,8 +345,9 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
let column = &workspace.columns[workspace.active_column_idx];
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
let width = column.width;
|
let width = column.width;
|
||||||
let is_full_width = column.is_full_width;
|
let is_full_width = column.is_full_width;
|
||||||
let window =
|
let window = workspace
|
||||||
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
|
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
|
||||||
|
.into_window();
|
||||||
|
|
||||||
self.add_window(new_idx, window, true, width, is_full_width);
|
self.add_window(new_idx, window, true, width, is_full_width);
|
||||||
}
|
}
|
||||||
@@ -309,8 +368,9 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
let column = &workspace.columns[workspace.active_column_idx];
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
let width = column.width;
|
let width = column.width;
|
||||||
let is_full_width = column.is_full_width;
|
let is_full_width = column.is_full_width;
|
||||||
let window =
|
let window = workspace
|
||||||
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
|
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
|
||||||
|
.into_window();
|
||||||
|
|
||||||
self.add_window(new_idx, window, true, width, is_full_width);
|
self.add_window(new_idx, window, true, width, is_full_width);
|
||||||
}
|
}
|
||||||
@@ -331,8 +391,9 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
let column = &workspace.columns[workspace.active_column_idx];
|
let column = &workspace.columns[workspace.active_column_idx];
|
||||||
let width = column.width;
|
let width = column.width;
|
||||||
let is_full_width = column.is_full_width;
|
let is_full_width = column.is_full_width;
|
||||||
let window =
|
let window = workspace
|
||||||
workspace.remove_window_by_idx(workspace.active_column_idx, column.active_tile_idx);
|
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
|
||||||
|
.into_window();
|
||||||
|
|
||||||
self.add_window(new_idx, window, true, width, is_full_width);
|
self.add_window(new_idx, window, true, width, is_full_width);
|
||||||
|
|
||||||
@@ -409,6 +470,11 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn previous_workspace_idx(&self) -> Option<usize> {
|
||||||
|
let id = self.previous_workspace_id?;
|
||||||
|
self.workspaces.iter().position(|w| w.id() == id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn switch_workspace(&mut self, idx: usize) {
|
pub fn switch_workspace(&mut self, idx: usize) {
|
||||||
self.activate_workspace(min(idx, self.workspaces.len() - 1));
|
self.activate_workspace(min(idx, self.workspaces.len() - 1));
|
||||||
// Don't animate this action.
|
// Don't animate this action.
|
||||||
@@ -417,6 +483,24 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
self.clean_up_workspaces();
|
self.clean_up_workspaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
|
||||||
|
let idx = min(idx, self.workspaces.len() - 1);
|
||||||
|
|
||||||
|
if idx == self.active_workspace_idx {
|
||||||
|
if let Some(prev_idx) = self.previous_workspace_idx() {
|
||||||
|
self.switch_workspace(prev_idx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.switch_workspace(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_workspace_previous(&mut self) {
|
||||||
|
if let Some(idx) = self.previous_workspace_idx() {
|
||||||
|
self.switch_workspace(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn consume_into_column(&mut self) {
|
pub fn consume_into_column(&mut self) {
|
||||||
self.active_workspace().consume_into_column();
|
self.active_workspace().consume_into_column();
|
||||||
}
|
}
|
||||||
@@ -462,7 +546,10 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
|
|
||||||
pub fn are_transitions_ongoing(&self) -> bool {
|
pub fn are_transitions_ongoing(&self) -> bool {
|
||||||
self.workspace_switch.is_some()
|
self.workspace_switch.is_some()
|
||||||
|| self.workspaces.iter().any(|ws| ws.are_animations_ongoing())
|
|| self
|
||||||
|
.workspaces
|
||||||
|
.iter()
|
||||||
|
.any(|ws| ws.are_transitions_ongoing())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(&mut self, options: Rc<Options>) {
|
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||||
@@ -512,8 +599,10 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
self.workspaces.push(ws);
|
self.workspaces.push(ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previous_workspace_id = self.previous_workspace_id;
|
||||||
self.activate_workspace(new_idx);
|
self.activate_workspace(new_idx);
|
||||||
self.workspace_switch = None;
|
self.workspace_switch = None;
|
||||||
|
self.previous_workspace_id = previous_workspace_id;
|
||||||
|
|
||||||
self.clean_up_workspaces();
|
self.clean_up_workspaces();
|
||||||
}
|
}
|
||||||
@@ -532,12 +621,33 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
self.workspaces.push(ws);
|
self.workspaces.push(ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previous_workspace_id = self.previous_workspace_id;
|
||||||
self.activate_workspace(new_idx);
|
self.activate_workspace(new_idx);
|
||||||
self.workspace_switch = None;
|
self.workspace_switch = None;
|
||||||
|
self.previous_workspace_id = previous_workspace_id;
|
||||||
|
|
||||||
self.clean_up_workspaces();
|
self.clean_up_workspaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the geometry of the active tile relative to and clamped to the output.
|
||||||
|
///
|
||||||
|
/// During animations, assumes the final view position.
|
||||||
|
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
|
||||||
|
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
|
||||||
|
|
||||||
|
if let Some(switch) = &self.workspace_switch {
|
||||||
|
let size = output_size(&self.output);
|
||||||
|
|
||||||
|
let offset = switch.target_idx() - self.active_workspace_idx as f64;
|
||||||
|
let offset = (offset * size.h as f64).round() as i32;
|
||||||
|
|
||||||
|
let clip_rect = Rectangle::from_loc_and_size((0, -offset), size);
|
||||||
|
rect = rect.intersection(clip_rect)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(rect)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn window_under(
|
pub fn window_under(
|
||||||
&self,
|
&self,
|
||||||
pos_within_output: Point<f64, Logical>,
|
pos_within_output: Point<f64, Logical>,
|
||||||
@@ -547,14 +657,28 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
let size = output_size(&self.output);
|
let size = output_size(&self.output);
|
||||||
|
|
||||||
let render_idx = switch.current_idx();
|
let render_idx = switch.current_idx();
|
||||||
let before_idx = render_idx.floor() as usize;
|
let before_idx = render_idx.floor();
|
||||||
let after_idx = render_idx.ceil() as usize;
|
let after_idx = render_idx.ceil();
|
||||||
|
|
||||||
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
|
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
|
||||||
|
|
||||||
|
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let after_idx = after_idx as usize;
|
||||||
|
|
||||||
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
|
let (idx, ws_offset) = if pos_within_output.y < (size.h - offset) as f64 {
|
||||||
(before_idx, Point::from((0, offset)))
|
if before_idx < 0. {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
(before_idx as usize, Point::from((0, offset)))
|
||||||
} else {
|
} else {
|
||||||
|
if after_idx >= self.workspaces.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
(after_idx, Point::from((0, -size.h + offset)))
|
(after_idx, Point::from((0, -size.h + offset)))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -578,16 +702,12 @@ impl<W: LayoutElement> Monitor<W> {
|
|||||||
let ws = &self.workspaces[self.active_workspace_idx];
|
let ws = &self.workspaces[self.active_workspace_idx];
|
||||||
ws.render_above_top_layer()
|
ws.render_above_top_layer()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Monitor<Window> {
|
pub fn render_elements<R: NiriRenderer>(
|
||||||
pub fn render_elements<R: Renderer + ImportAll>(
|
|
||||||
&self,
|
&self,
|
||||||
renderer: &mut R,
|
renderer: &mut R,
|
||||||
) -> Vec<MonitorRenderElement<R>>
|
target: RenderTarget,
|
||||||
where
|
) -> Vec<MonitorRenderElement<R>> {
|
||||||
<R as Renderer>::TextureId: 'static,
|
|
||||||
{
|
|
||||||
let _span = tracy_client::span!("Monitor::render_elements");
|
let _span = tracy_client::span!("Monitor::render_elements");
|
||||||
|
|
||||||
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
|
let output_scale = Scale::from(self.output.current_scale().fractional_scale());
|
||||||
@@ -598,40 +718,67 @@ impl Monitor<Window> {
|
|||||||
match &self.workspace_switch {
|
match &self.workspace_switch {
|
||||||
Some(switch) => {
|
Some(switch) => {
|
||||||
let render_idx = switch.current_idx();
|
let render_idx = switch.current_idx();
|
||||||
let before_idx = render_idx.floor() as usize;
|
let before_idx = render_idx.floor();
|
||||||
let after_idx = render_idx.ceil() as usize;
|
let after_idx = render_idx.ceil();
|
||||||
|
|
||||||
let offset = ((render_idx - before_idx as f64) * size.h as f64).round() as i32;
|
let offset = ((render_idx - before_idx) * size.h as f64).round() as i32;
|
||||||
|
|
||||||
let before = self.workspaces[before_idx].render_elements(renderer);
|
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||||
let after = self.workspaces[after_idx].render_elements(renderer);
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let after_idx = after_idx as usize;
|
||||||
|
let after = if after_idx < self.workspaces.len() {
|
||||||
|
let after = self.workspaces[after_idx].render_elements(renderer, target);
|
||||||
|
let after = after.into_iter().filter_map(|elem| {
|
||||||
|
Some(RelocateRenderElement::from_element(
|
||||||
|
CropRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
output_scale,
|
||||||
|
// HACK: crop to infinite bounds for all sides except the side
|
||||||
|
// where the workspaces join,
|
||||||
|
// otherwise it will cut pixel shaders and mess up
|
||||||
|
// the coordinate space.
|
||||||
|
Rectangle::from_extemities(
|
||||||
|
(-i32::MAX / 2, 0),
|
||||||
|
(i32::MAX / 2, i32::MAX / 2),
|
||||||
|
),
|
||||||
|
)?,
|
||||||
|
(0, -offset + size.h),
|
||||||
|
Relocate::Relative,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
if before_idx < 0. {
|
||||||
|
return after.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(after)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let before_idx = before_idx as usize;
|
||||||
|
let before = self.workspaces[before_idx].render_elements(renderer, target);
|
||||||
let before = before.into_iter().filter_map(|elem| {
|
let before = before.into_iter().filter_map(|elem| {
|
||||||
Some(RelocateRenderElement::from_element(
|
Some(RelocateRenderElement::from_element(
|
||||||
CropRenderElement::from_element(
|
CropRenderElement::from_element(
|
||||||
elem,
|
elem,
|
||||||
output_scale,
|
output_scale,
|
||||||
Rectangle::from_extemities((0, offset), (size.w, size.h)),
|
Rectangle::from_extemities(
|
||||||
|
(-i32::MAX / 2, -i32::MAX / 2),
|
||||||
|
(i32::MAX / 2, size.h),
|
||||||
|
),
|
||||||
)?,
|
)?,
|
||||||
(0, -offset),
|
(0, -offset),
|
||||||
Relocate::Relative,
|
Relocate::Relative,
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
let after = after.into_iter().filter_map(|elem| {
|
before.chain(after.into_iter().flatten()).collect()
|
||||||
Some(RelocateRenderElement::from_element(
|
|
||||||
CropRenderElement::from_element(
|
|
||||||
elem,
|
|
||||||
output_scale,
|
|
||||||
Rectangle::from_extemities((0, 0), (size.w, offset)),
|
|
||||||
)?,
|
|
||||||
(0, -offset + size.h),
|
|
||||||
Relocate::Relative,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
before.chain(after).collect()
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let elements = self.workspaces[self.active_workspace_idx].render_elements(renderer);
|
let elements =
|
||||||
|
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
|
||||||
elements
|
elements
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|elem| {
|
.filter_map(|elem| {
|
||||||
@@ -656,4 +803,87 @@ impl Monitor<Window> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn workspace_switch_gesture_begin(&mut self) {
|
||||||
|
let center_idx = self.active_workspace_idx;
|
||||||
|
let current_idx = self
|
||||||
|
.workspace_switch
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.current_idx())
|
||||||
|
.unwrap_or(center_idx as f64);
|
||||||
|
|
||||||
|
let gesture = WorkspaceSwitchGesture {
|
||||||
|
center_idx,
|
||||||
|
current_idx,
|
||||||
|
tracker: SwipeTracker::new(),
|
||||||
|
};
|
||||||
|
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_switch_gesture_update(
|
||||||
|
&mut self,
|
||||||
|
delta_y: f64,
|
||||||
|
timestamp: Duration,
|
||||||
|
) -> Option<bool> {
|
||||||
|
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
gesture.tracker.push(delta_y, timestamp);
|
||||||
|
|
||||||
|
let pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||||
|
|
||||||
|
let min = gesture.center_idx.saturating_sub(1) as f64;
|
||||||
|
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
|
||||||
|
let new_idx = gesture.center_idx as f64 + pos;
|
||||||
|
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
||||||
|
|
||||||
|
if gesture.current_idx == new_idx {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
gesture.current_idx = new_idx;
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_switch_gesture_end(&mut self, cancelled: bool) -> bool {
|
||||||
|
let Some(WorkspaceSwitch::Gesture(gesture)) = &mut self.workspace_switch else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if cancelled {
|
||||||
|
self.workspace_switch = None;
|
||||||
|
self.clean_up_workspaces();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut velocity = gesture.tracker.velocity() / WORKSPACE_GESTURE_MOVEMENT;
|
||||||
|
let current_pos = gesture.tracker.pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||||
|
let pos = gesture.tracker.projected_end_pos() / WORKSPACE_GESTURE_MOVEMENT;
|
||||||
|
|
||||||
|
let min = gesture.center_idx.saturating_sub(1) as f64;
|
||||||
|
let max = (gesture.center_idx + 1).min(self.workspaces.len() - 1) as f64;
|
||||||
|
let new_idx = gesture.center_idx as f64 + pos;
|
||||||
|
|
||||||
|
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
||||||
|
let new_idx = new_idx.round() as usize;
|
||||||
|
|
||||||
|
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
gesture.center_idx as f64 + current_pos,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
|
||||||
|
|
||||||
|
self.active_workspace_idx = new_idx;
|
||||||
|
self.workspace_switch = Some(WorkspaceSwitch::Animation(Animation::new(
|
||||||
|
gesture.current_idx,
|
||||||
|
new_idx as f64,
|
||||||
|
velocity,
|
||||||
|
self.options.animations.workspace_switch.0,
|
||||||
|
)));
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+516
-37
@@ -2,15 +2,27 @@ use std::cmp::max;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
use smithay::backend::renderer::element::utils::RescaleRenderElement;
|
||||||
use smithay::backend::renderer::element::Kind;
|
use smithay::backend::renderer::element::{Element, Kind};
|
||||||
use smithay::backend::renderer::{ImportAll, Renderer};
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||||
|
|
||||||
use super::focus_ring::FocusRing;
|
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||||
use super::workspace::WorkspaceRenderElement;
|
use super::{
|
||||||
use super::{LayoutElement, Options};
|
LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, Options,
|
||||||
|
RESIZE_ANIMATION_THRESHOLD,
|
||||||
|
};
|
||||||
|
use crate::animation::Animation;
|
||||||
|
use crate::niri_render_elements;
|
||||||
|
use crate::render_helpers::crossfade::CrossfadeRenderElement;
|
||||||
|
use crate::render_helpers::offscreen::OffscreenRenderElement;
|
||||||
|
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::shaders::Shaders;
|
||||||
|
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||||
|
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget, ToRenderElement};
|
||||||
|
|
||||||
/// Toplevel window with decorations.
|
/// Toplevel window with decorations.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -21,6 +33,12 @@ pub struct Tile<W: LayoutElement> {
|
|||||||
/// The border around the window.
|
/// The border around the window.
|
||||||
border: FocusRing,
|
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,
|
||||||
|
|
||||||
/// Whether this tile is fullscreen.
|
/// Whether this tile is fullscreen.
|
||||||
///
|
///
|
||||||
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
/// This will update only when the `window` actually goes fullscreen, rather than right away,
|
||||||
@@ -33,24 +51,80 @@ pub struct Tile<W: LayoutElement> {
|
|||||||
/// The size we were requested to fullscreen into.
|
/// The size we were requested to fullscreen into.
|
||||||
fullscreen_size: Size<i32, Logical>,
|
fullscreen_size: Size<i32, Logical>,
|
||||||
|
|
||||||
|
/// The animation upon opening a window.
|
||||||
|
open_animation: Option<Animation>,
|
||||||
|
|
||||||
|
/// The animation of the window resizing.
|
||||||
|
resize_animation: Option<ResizeAnimation>,
|
||||||
|
|
||||||
|
/// The animation of a tile visually moving horizontally.
|
||||||
|
move_x_animation: Option<MoveAnimation>,
|
||||||
|
|
||||||
|
/// The animation of a tile visually moving vertically.
|
||||||
|
move_y_animation: Option<MoveAnimation>,
|
||||||
|
|
||||||
/// Configurable properties of the layout.
|
/// Configurable properties of the layout.
|
||||||
options: Rc<Options>,
|
pub options: Rc<Options>,
|
||||||
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
TileRenderElement<R> => {
|
||||||
|
LayoutElement = LayoutElementRenderElement<R>,
|
||||||
|
FocusRing = FocusRingRenderElement,
|
||||||
|
SolidColor = SolidColorRenderElement,
|
||||||
|
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
|
||||||
|
Crossfade = CrossfadeRenderElement,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
TileSnapshotContentsRenderElement => {
|
||||||
|
Texture = PrimaryGpuTextureRenderElement,
|
||||||
|
SolidColor = SolidColorRenderElement,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
TileSnapshotRenderElement => {
|
||||||
|
Contents = RescaleRenderElement<TileSnapshotContentsRenderElement>,
|
||||||
|
FocusRing = FocusRingRenderElement,
|
||||||
|
SolidColor = SolidColorRenderElement,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ResizeAnimation {
|
||||||
|
anim: Animation,
|
||||||
|
size_from: Size<i32, Logical>,
|
||||||
|
snapshot: LayoutElementRenderSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MoveAnimation {
|
||||||
|
anim: Animation,
|
||||||
|
from: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: LayoutElement> Tile<W> {
|
impl<W: LayoutElement> Tile<W> {
|
||||||
pub fn new(window: W, options: Rc<Options>) -> Self {
|
pub fn new(window: W, options: Rc<Options>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
border: FocusRing::new(options.border),
|
border: FocusRing::new(options.border.into()),
|
||||||
|
focus_ring: FocusRing::new(options.focus_ring),
|
||||||
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size.
|
||||||
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
|
fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]),
|
||||||
fullscreen_size: Default::default(),
|
fullscreen_size: Default::default(),
|
||||||
|
open_animation: None,
|
||||||
|
resize_animation: None,
|
||||||
|
move_x_animation: None,
|
||||||
|
move_y_animation: None,
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(&mut self, options: Rc<Options>) {
|
pub fn update_config(&mut self, options: Rc<Options>) {
|
||||||
self.border.update_config(options.border);
|
self.border.update_config(options.border.into());
|
||||||
|
self.focus_ring.update_config(options.focus_ring);
|
||||||
self.options = options;
|
self.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,26 +133,182 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
if self.fullscreen_size != Size::from((0, 0)) {
|
if self.fullscreen_size != Size::from((0, 0)) {
|
||||||
self.is_fullscreen = self.window.is_fullscreen();
|
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() {
|
||||||
|
// Compute like in animated_window_size(), but using the snapshot geometry (since
|
||||||
|
// the current one is already overwritten).
|
||||||
|
let mut size = animate_from.size;
|
||||||
|
|
||||||
|
let val = resize.anim.value();
|
||||||
|
let size_from = resize.size_from;
|
||||||
|
|
||||||
|
size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
|
||||||
|
size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
|
||||||
|
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
animate_from.size
|
||||||
|
};
|
||||||
|
|
||||||
|
let change = self.window.size().to_point() - size_from.to_point();
|
||||||
|
let change = max(change.x.abs(), change.y.abs());
|
||||||
|
if change > RESIZE_ANIMATION_THRESHOLD {
|
||||||
|
let anim = Animation::new(0., 1., 0., self.options.animations.window_resize.0);
|
||||||
|
self.resize_animation = Some(ResizeAnimation {
|
||||||
|
anim,
|
||||||
|
size_from,
|
||||||
|
snapshot: animate_from,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.resize_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) {
|
pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) {
|
||||||
let width = self.border.width();
|
if let Some(anim) = &mut self.open_animation {
|
||||||
self.border.update(
|
anim.set_current_time(current_time);
|
||||||
(width, width).into(),
|
if anim.is_done() {
|
||||||
self.window.size(),
|
self.open_animation = None;
|
||||||
self.window.has_ssd(),
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if let Some(resize) = &mut self.resize_animation {
|
||||||
|
resize.anim.set_current_time(current_time);
|
||||||
|
if resize.anim.is_done() {
|
||||||
|
self.resize_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(move_) = &mut self.move_x_animation {
|
||||||
|
move_.anim.set_current_time(current_time);
|
||||||
|
if move_.anim.is_done() {
|
||||||
|
self.move_x_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(move_) = &mut self.move_y_animation {
|
||||||
|
move_.anim.set_current_time(current_time);
|
||||||
|
if move_.anim.is_done() {
|
||||||
|
self.move_y_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let draw_border_with_background = self
|
||||||
|
.window
|
||||||
|
.rules()
|
||||||
|
.draw_border_with_background
|
||||||
|
.unwrap_or_else(|| !self.window.has_ssd());
|
||||||
|
self.border
|
||||||
|
.update(self.animated_window_size(), !draw_border_with_background);
|
||||||
self.border.set_active(is_active);
|
self.border.set_active(is_active);
|
||||||
|
|
||||||
|
let draw_focus_ring_with_background = if self.effective_border_width().is_some() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
draw_border_with_background
|
||||||
|
};
|
||||||
|
self.focus_ring
|
||||||
|
.update(self.animated_tile_size(), !draw_focus_ring_with_background);
|
||||||
|
self.focus_ring.set_active(is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_offset(&self) -> Point<i32, Logical> {
|
||||||
|
let mut offset = Point::from((0., 0.));
|
||||||
|
|
||||||
|
if let Some(move_) = &self.move_x_animation {
|
||||||
|
offset.x += f64::from(move_.from) * move_.anim.value();
|
||||||
|
}
|
||||||
|
if let Some(move_) = &self.move_y_animation {
|
||||||
|
offset.y += f64::from(move_.from) * move_.anim.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
offset.to_i32_round()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_open_animation(&mut self) {
|
||||||
|
self.open_animation = Some(Animation::new(
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
0.,
|
||||||
|
self.options.animations.window_open.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_animation(&self) -> &Option<Animation> {
|
||||||
|
&self.open_animation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_animation(&self) -> Option<&Animation> {
|
||||||
|
self.resize_animation.as_ref().map(|resize| &resize.anim)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate_move_from(&mut self, from: Point<i32, Logical>) {
|
||||||
|
self.animate_move_x_from(from.x);
|
||||||
|
self.animate_move_y_from(from.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate_move_x_from(&mut self, from: i32) {
|
||||||
|
self.animate_move_x_from_with_config(from, self.options.animations.window_movement.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate_move_x_from_with_config(&mut self, from: i32, config: niri_config::Animation) {
|
||||||
|
let current_offset = self.render_offset().x;
|
||||||
|
|
||||||
|
// Preserve the previous config if ongoing.
|
||||||
|
let anim = self.move_x_animation.take().map(|move_| move_.anim);
|
||||||
|
let anim = anim
|
||||||
|
.map(|anim| anim.restarted(1., 0., 0.))
|
||||||
|
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
|
||||||
|
|
||||||
|
self.move_x_animation = Some(MoveAnimation {
|
||||||
|
anim,
|
||||||
|
from: from + current_offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate_move_y_from(&mut self, from: i32) {
|
||||||
|
self.animate_move_y_from_with_config(from, self.options.animations.window_movement.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animate_move_y_from_with_config(&mut self, from: i32, config: niri_config::Animation) {
|
||||||
|
let current_offset = self.render_offset().y;
|
||||||
|
|
||||||
|
// Preserve the previous config if ongoing.
|
||||||
|
let anim = self.move_y_animation.take().map(|move_| move_.anim);
|
||||||
|
let anim = anim
|
||||||
|
.map(|anim| anim.restarted(1., 0., 0.))
|
||||||
|
.unwrap_or_else(|| Animation::new(1., 0., 0., config));
|
||||||
|
|
||||||
|
self.move_y_animation = Some(MoveAnimation {
|
||||||
|
anim,
|
||||||
|
from: from + current_offset,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn window(&self) -> &W {
|
pub fn window(&self) -> &W {
|
||||||
&self.window
|
&self.window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn window_mut(&mut self) -> &mut W {
|
||||||
|
&mut self.window
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_window(self) -> W {
|
pub fn into_window(self) -> W {
|
||||||
self.window
|
self.window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_fullscreen(&self) -> bool {
|
||||||
|
self.is_fullscreen
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
|
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
|
||||||
fn effective_border_width(&self) -> Option<i32> {
|
fn effective_border_width(&self) -> Option<i32> {
|
||||||
if self.is_fullscreen {
|
if self.is_fullscreen {
|
||||||
@@ -141,6 +371,41 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
self.window.size()
|
self.window.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn animated_window_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = self.window.size();
|
||||||
|
|
||||||
|
if let Some(resize) = &self.resize_animation {
|
||||||
|
let val = resize.anim.value();
|
||||||
|
let size_from = resize.size_from;
|
||||||
|
|
||||||
|
size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32;
|
||||||
|
size.w = max(1, size.w);
|
||||||
|
size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32;
|
||||||
|
size.h = max(1, size.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animated_tile_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = self.animated_window_size();
|
||||||
|
|
||||||
|
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 = max(size.w, self.fullscreen_size.w);
|
||||||
|
size.h = max(size.h, self.fullscreen_size.h);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
|
size.w = size.w.saturating_add(width * 2);
|
||||||
|
size.h = size.h.saturating_add(width * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
pub fn buf_loc(&self) -> Point<i32, Logical> {
|
pub fn buf_loc(&self) -> Point<i32, Logical> {
|
||||||
let mut loc = Point::from((0, 0));
|
let mut loc = Point::from((0, 0));
|
||||||
loc += self.window_loc();
|
loc += self.window_loc();
|
||||||
@@ -158,7 +423,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
activation_region.to_f64().contains(point)
|
activation_region.to_f64().contains(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>) {
|
pub fn request_tile_size(&mut self, mut size: Size<i32, Logical>, animate: bool) {
|
||||||
// Can't go through effective_border_width() because we might be fullscreen.
|
// Can't go through effective_border_width() because we might be fullscreen.
|
||||||
if !self.border.is_off() {
|
if !self.border.is_off() {
|
||||||
let width = self.border.width();
|
let width = self.border.width();
|
||||||
@@ -166,7 +431,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size.h = max(1, size.h - width * 2);
|
size.h = max(1, size.h - width * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window.request_size(size);
|
self.window.request_size(size, animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
|
pub fn tile_width_for_window_width(&self, size: i32) -> i32 {
|
||||||
@@ -228,35 +493,226 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_ssd(&self) -> bool {
|
pub fn draw_border_with_background(&self) -> bool {
|
||||||
self.effective_border_width().is_some() || self.window.has_ssd()
|
if self.effective_border_width().is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window
|
||||||
|
.rules()
|
||||||
|
.draw_border_with_background
|
||||||
|
.unwrap_or_else(|| !self.window.has_ssd())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<R: Renderer + ImportAll>(
|
fn render_inner<R: NiriRenderer>(
|
||||||
&self,
|
&self,
|
||||||
renderer: &mut R,
|
renderer: &mut R,
|
||||||
location: Point<i32, Logical>,
|
location: Point<i32, Logical>,
|
||||||
scale: Scale<f64>,
|
scale: Scale<f64>,
|
||||||
) -> Vec<WorkspaceRenderElement<R>>
|
view_size: Size<i32, Logical>,
|
||||||
|
focus_ring: bool,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||||
|
let alpha = if self.is_fullscreen {
|
||||||
|
1.
|
||||||
|
} else {
|
||||||
|
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_loc = self.window_loc();
|
||||||
|
let window_size = self.window_size();
|
||||||
|
let animated_window_size = self.animated_window_size();
|
||||||
|
let window_render_loc = location + window_loc;
|
||||||
|
let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size);
|
||||||
|
|
||||||
|
let gles_renderer = renderer.as_gles_renderer();
|
||||||
|
|
||||||
|
// If we're resizing, try to render a crossfade, or a fallback.
|
||||||
|
let mut crossfade = None;
|
||||||
|
let mut crossfade_fallback = None;
|
||||||
|
|
||||||
|
if let Some(resize) = &self.resize_animation {
|
||||||
|
if Shaders::get(gles_renderer).crossfade.is_some() {
|
||||||
|
if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
|
||||||
|
let window_elements =
|
||||||
|
self.window
|
||||||
|
.render(gles_renderer, Point::from((0, 0)), scale, 1., 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();
|
||||||
|
|
||||||
|
if let Some((texture_current, _sync_point, texture_current_geo)) = current {
|
||||||
|
let elem = CrossfadeRenderElement::new(
|
||||||
|
gles_renderer,
|
||||||
|
area,
|
||||||
|
scale,
|
||||||
|
texture_from.clone(),
|
||||||
|
resize.snapshot.size,
|
||||||
|
(texture_current, texture_current_geo),
|
||||||
|
window_size,
|
||||||
|
resize.anim.clamped_value().clamp(0., 1.) as f32,
|
||||||
|
alpha,
|
||||||
|
)
|
||||||
|
.expect("we checked the crossfade shader above");
|
||||||
|
self.window
|
||||||
|
.set_offscreen_element_id(Some(elem.id().clone()));
|
||||||
|
crossfade = Some(elem.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if crossfade.is_none() {
|
||||||
|
let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]);
|
||||||
|
crossfade_fallback = Some(
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
&fallback_buffer,
|
||||||
|
area.loc.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
self.window.set_offscreen_element_id(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not resizing, render the window itself.
|
||||||
|
let mut window = None;
|
||||||
|
if crossfade.is_none() && crossfade_fallback.is_none() {
|
||||||
|
window = Some(
|
||||||
|
self.window
|
||||||
|
.render(renderer, window_render_loc, scale, alpha, target)
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rv = crossfade
|
||||||
|
.into_iter()
|
||||||
|
.chain(crossfade_fallback)
|
||||||
|
.chain(window.into_iter().flatten());
|
||||||
|
|
||||||
|
let elem = self.effective_border_width().map(|width| {
|
||||||
|
self.border
|
||||||
|
.render(
|
||||||
|
renderer,
|
||||||
|
location + Point::from((width, width)),
|
||||||
|
scale,
|
||||||
|
view_size,
|
||||||
|
)
|
||||||
|
.map(Into::into)
|
||||||
|
});
|
||||||
|
let rv = rv.chain(elem.into_iter().flatten());
|
||||||
|
|
||||||
|
let elem = focus_ring.then(|| {
|
||||||
|
self.focus_ring
|
||||||
|
.render(renderer, location, scale, view_size)
|
||||||
|
.map(Into::into)
|
||||||
|
});
|
||||||
|
let rv = rv.chain(elem.into_iter().flatten());
|
||||||
|
|
||||||
|
let elem = self.is_fullscreen.then(|| {
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
&self.fullscreen_backdrop,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
1.,
|
||||||
|
Kind::Unspecified,
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
rv.chain(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
view_size: Size<i32, Logical>,
|
||||||
|
focus_ring: bool,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||||
|
if let Some(anim) = &self.open_animation {
|
||||||
|
let renderer = renderer.as_gles_renderer();
|
||||||
|
let elements =
|
||||||
|
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
|
||||||
|
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
|
||||||
|
|
||||||
|
let elem = OffscreenRenderElement::new(
|
||||||
|
renderer,
|
||||||
|
scale.x as i32,
|
||||||
|
&elements,
|
||||||
|
anim.clamped_value().clamp(0., 1.) as f32,
|
||||||
|
);
|
||||||
|
self.window()
|
||||||
|
.set_offscreen_element_id(Some(elem.id().clone()));
|
||||||
|
|
||||||
|
let mut center = location;
|
||||||
|
center.x += self.tile_size().w / 2;
|
||||||
|
center.y += self.tile_size().h / 2;
|
||||||
|
|
||||||
|
Some(TileRenderElement::Offscreen(
|
||||||
|
RescaleRenderElement::from_element(
|
||||||
|
elem,
|
||||||
|
center.to_physical_precise_round(scale),
|
||||||
|
(anim.value() / 2. + 0.5).max(0.),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.into_iter()
|
||||||
|
.chain(None.into_iter().flatten())
|
||||||
|
} else {
|
||||||
|
self.window().set_offscreen_element_id(None);
|
||||||
|
|
||||||
|
let elements =
|
||||||
|
self.render_inner(renderer, location, scale, view_size, focus_ring, target);
|
||||||
|
None.into_iter().chain(Some(elements).into_iter().flatten())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_snapshot<E, C>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
view_size: Size<i32, Logical>,
|
||||||
|
contents: Vec<C>,
|
||||||
|
) -> Vec<TileSnapshotRenderElement>
|
||||||
where
|
where
|
||||||
<R as Renderer>::TextureId: 'static,
|
E: Into<TileSnapshotContentsRenderElement>,
|
||||||
|
C: ToRenderElement<RenderElement = E>,
|
||||||
{
|
{
|
||||||
let mut rv = Vec::new();
|
let alpha = if self.is_fullscreen {
|
||||||
|
1.
|
||||||
|
} else {
|
||||||
|
self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.)
|
||||||
|
};
|
||||||
|
|
||||||
let window_pos = location + self.window_loc();
|
let window_size = self.window_size();
|
||||||
rv.extend(self.window.render(renderer, window_pos, scale));
|
let animated_window_size = self.animated_window_size();
|
||||||
|
let animated_scale = animated_window_size.to_f64() / window_size.to_f64();
|
||||||
|
|
||||||
if self.effective_border_width().is_some() {
|
let mut rv = vec![];
|
||||||
|
|
||||||
|
for baked in contents {
|
||||||
|
let elem = baked.to_render_element(self.window_loc(), scale, alpha, Kind::Unspecified);
|
||||||
|
let elem: TileSnapshotContentsRenderElement = elem.into();
|
||||||
|
|
||||||
|
let origin = self.window_loc().to_physical_precise_round(scale);
|
||||||
|
let elem = RescaleRenderElement::from_element(elem, origin, animated_scale);
|
||||||
|
rv.push(elem.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(width) = self.effective_border_width() {
|
||||||
rv.extend(
|
rv.extend(
|
||||||
self.border
|
self.border
|
||||||
.render(scale)
|
.render(renderer, Point::from((width, width)), scale, view_size)
|
||||||
.map(|elem| {
|
|
||||||
RelocateRenderElement::from_element(
|
|
||||||
elem,
|
|
||||||
location.to_physical_precise_round(scale),
|
|
||||||
Relocate::Relative,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(Into::into),
|
.map(Into::into),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -264,7 +720,7 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
if self.is_fullscreen {
|
if self.is_fullscreen {
|
||||||
let elem = SolidColorRenderElement::from_buffer(
|
let elem = SolidColorRenderElement::from_buffer(
|
||||||
&self.fullscreen_backdrop,
|
&self.fullscreen_backdrop,
|
||||||
location.to_physical_precise_round(scale),
|
Point::from((0, 0)),
|
||||||
scale,
|
scale,
|
||||||
1.,
|
1.,
|
||||||
Kind::Unspecified,
|
Kind::Unspecified,
|
||||||
@@ -274,4 +730,27 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
|
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_snapshot_for_close_anim(
|
||||||
|
&self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
view_size: Size<i32, Logical>,
|
||||||
|
) -> Option<RenderSnapshot<TileSnapshotRenderElement, TileSnapshotRenderElement>> {
|
||||||
|
let snapshot = self.window.take_unmap_snapshot()?;
|
||||||
|
|
||||||
|
Some(RenderSnapshot {
|
||||||
|
contents: self.render_snapshot(renderer, scale, view_size, snapshot.contents),
|
||||||
|
blocked_out_contents: self.render_snapshot(
|
||||||
|
renderer,
|
||||||
|
scale,
|
||||||
|
view_size,
|
||||||
|
snapshot.blocked_out_contents,
|
||||||
|
),
|
||||||
|
block_out_from: snapshot.block_out_from,
|
||||||
|
size: self.animated_tile_size(),
|
||||||
|
texture: Default::default(),
|
||||||
|
blocked_out_texture: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1448
-286
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate tracing;
|
||||||
|
|
||||||
|
pub mod animation;
|
||||||
|
pub mod backend;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod cursor;
|
||||||
|
#[cfg(feature = "dbus")]
|
||||||
|
pub mod dbus;
|
||||||
|
pub mod frame_clock;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod input;
|
||||||
|
pub mod ipc;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod niri;
|
||||||
|
pub mod protocols;
|
||||||
|
pub mod render_helpers;
|
||||||
|
pub mod rubber_band;
|
||||||
|
pub mod scroll_tracker;
|
||||||
|
pub mod swipe_tracker;
|
||||||
|
pub mod ui;
|
||||||
|
pub mod utils;
|
||||||
|
pub mod window;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||||
|
pub mod dummy_pw_utils;
|
||||||
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
|
pub mod pw_utils;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
||||||
|
pub use dummy_pw_utils as pw_utils;
|
||||||
+137
-108
@@ -1,95 +1,33 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate tracing;
|
extern crate tracing;
|
||||||
|
|
||||||
mod animation;
|
use std::fmt::Write as _;
|
||||||
mod backend;
|
use std::fs::{self, File};
|
||||||
mod config_error_notification;
|
use std::io::{self, Write};
|
||||||
mod cursor;
|
use std::os::fd::FromRawFd;
|
||||||
#[cfg(feature = "dbus")]
|
|
||||||
mod dbus;
|
|
||||||
mod exit_confirm_dialog;
|
|
||||||
mod frame_clock;
|
|
||||||
mod handlers;
|
|
||||||
mod hotkey_overlay;
|
|
||||||
mod input;
|
|
||||||
mod ipc;
|
|
||||||
mod layout;
|
|
||||||
mod niri;
|
|
||||||
mod render_helpers;
|
|
||||||
mod screenshot_ui;
|
|
||||||
mod utils;
|
|
||||||
mod watcher;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
|
||||||
mod dummy_pw_utils;
|
|
||||||
#[cfg(feature = "xdp-gnome-screencast")]
|
|
||||||
mod pw_utils;
|
|
||||||
|
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::{env, mem};
|
use std::{env, mem};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::Parser;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
#[cfg(not(feature = "xdp-gnome-screencast"))]
|
use niri::animation;
|
||||||
use dummy_pw_utils as pw_utils;
|
use niri::cli::{Cli, Sub};
|
||||||
use git_version::git_version;
|
#[cfg(feature = "dbus")]
|
||||||
use niri::{Niri, State};
|
use niri::dbus;
|
||||||
|
use niri::ipc::client::handle_msg;
|
||||||
|
use niri::niri::State;
|
||||||
|
use niri::utils::spawning::{
|
||||||
|
spawn, CHILD_ENV, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||||
|
};
|
||||||
|
use niri::utils::watcher::Watcher;
|
||||||
|
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
|
||||||
use niri_config::Config;
|
use niri_config::Config;
|
||||||
use portable_atomic::Ordering;
|
use portable_atomic::Ordering;
|
||||||
use sd_notify::NotifyState;
|
use sd_notify::NotifyState;
|
||||||
use smithay::reexports::calloop::{self, EventLoop};
|
use smithay::reexports::calloop::EventLoop;
|
||||||
use smithay::reexports::wayland_server::Display;
|
use smithay::reexports::wayland_server::Display;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use utils::spawn;
|
|
||||||
use watcher::Watcher;
|
|
||||||
|
|
||||||
use crate::ipc::client::handle_msg;
|
|
||||||
use crate::utils::{cause_panic, REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(author, version = version(), about, long_about = None)]
|
|
||||||
#[command(args_conflicts_with_subcommands = true)]
|
|
||||||
#[command(subcommand_value_name = "SUBCOMMAND")]
|
|
||||||
#[command(subcommand_help_heading = "Subcommands")]
|
|
||||||
struct Cli {
|
|
||||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
|
||||||
#[arg(short, long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
/// Command to run upon compositor startup.
|
|
||||||
#[arg(last = true)]
|
|
||||||
command: Vec<OsString>,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
|
||||||
subcommand: Option<Sub>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Sub {
|
|
||||||
/// Validate the config file.
|
|
||||||
Validate {
|
|
||||||
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
|
|
||||||
#[arg(short, long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
},
|
|
||||||
/// Communicate with the running niri instance.
|
|
||||||
Msg {
|
|
||||||
#[command(subcommand)]
|
|
||||||
msg: Msg,
|
|
||||||
/// Format output as JSON.
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
},
|
|
||||||
/// Cause a panic to check if the backtraces are good.
|
|
||||||
Panic,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Msg {
|
|
||||||
/// List connected outputs.
|
|
||||||
Outputs,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Set backtrace defaults if not set.
|
// Set backtrace defaults if not set.
|
||||||
@@ -102,7 +40,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
|
REMOVE_ENV_RUST_LIB_BACKTRACE.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_systemd_service = env::var_os("NOTIFY_SOCKET").is_some();
|
if env::var_os("NOTIFY_SOCKET").is_some() {
|
||||||
|
IS_SYSTEMD_SERVICE.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "systemd"))]
|
||||||
|
warn!(
|
||||||
|
"running as a systemd service, but systemd support is compiled out. \
|
||||||
|
Are you sure you did not forget to set `--features systemd`?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
|
let directives = env::var("RUST_LOG").unwrap_or_else(|_| "niri=debug".to_owned());
|
||||||
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
let env_filter = EnvFilter::builder().parse_lossy(directives);
|
||||||
@@ -111,21 +57,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.with_env_filter(env_filter)
|
.with_env_filter(env_filter)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
if is_systemd_service {
|
let cli = Cli::parse();
|
||||||
// If we're starting as a systemd service, 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
|
if cli.session {
|
||||||
// cause the winit backend to be selected instead.
|
// 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.
|
||||||
if env::var_os("DISPLAY").is_some() {
|
if env::var_os("DISPLAY").is_some() {
|
||||||
debug!("we're running as a systemd service but DISPLAY is set, removing it");
|
warn!("running as a session but DISPLAY is set, removing it");
|
||||||
env::remove_var("DISPLAY");
|
env::remove_var("DISPLAY");
|
||||||
}
|
}
|
||||||
if env::var_os("WAYLAND_DISPLAY").is_some() {
|
if env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||||
debug!("we're running as a systemd service but WAYLAND_DISPLAY is set, removing it");
|
warn!("running as a session but WAYLAND_DISPLAY is set, removing it");
|
||||||
env::remove_var("WAYLAND_DISPLAY");
|
env::remove_var("WAYLAND_DISPLAY");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
// Set the current desktop for xdg-desktop-portal.
|
||||||
|
env::set_var("XDG_CURRENT_DESKTOP", "niri");
|
||||||
|
// Ensure the session type is set to Wayland for xdg-autostart and Qt apps.
|
||||||
|
env::set_var("XDG_SESSION_TYPE", "wayland");
|
||||||
|
}
|
||||||
|
|
||||||
let _client = tracy_client::Client::start();
|
let _client = tracy_client::Client::start();
|
||||||
|
|
||||||
@@ -154,7 +105,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
info!("starting version {}", &version());
|
info!("starting version {}", &version());
|
||||||
|
|
||||||
// Load the config.
|
// Load the config.
|
||||||
let path = cli.config.or_else(default_config_path);
|
let mut config_created = false;
|
||||||
|
let path = cli.config.or_else(|| {
|
||||||
|
let default_path = default_config_path()?;
|
||||||
|
let default_parent = default_path.parent().unwrap();
|
||||||
|
|
||||||
|
if let Err(err) = fs::create_dir_all(default_parent) {
|
||||||
|
warn!(
|
||||||
|
"error creating config directories {:?}: {err:?}",
|
||||||
|
default_parent
|
||||||
|
);
|
||||||
|
return Some(default_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the config and fill it with the default config if it doesn't exist.
|
||||||
|
let new_file = File::options()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(&default_path);
|
||||||
|
match new_file {
|
||||||
|
Ok(mut new_file) => {
|
||||||
|
let default = include_bytes!("../resources/default-config.kdl");
|
||||||
|
match new_file.write_all(default) {
|
||||||
|
Ok(()) => {
|
||||||
|
config_created = true;
|
||||||
|
info!("wrote default config to {:?}", &default_path);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error writing config file at {:?}: {err:?}", &default_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||||
|
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(default_path)
|
||||||
|
});
|
||||||
|
|
||||||
let mut config_errored = false;
|
let mut config_errored = false;
|
||||||
let mut config = path
|
let mut config = path
|
||||||
@@ -169,8 +157,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
|
let slowdown = if config.animations.off {
|
||||||
|
0.
|
||||||
|
} else {
|
||||||
|
config.animations.slowdown.clamp(0., 100.)
|
||||||
|
};
|
||||||
|
animation::ANIMATION_SLOWDOWN.store(slowdown, Ordering::Relaxed);
|
||||||
|
|
||||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||||
|
*CHILD_ENV.write().unwrap() = mem::take(&mut config.environment);
|
||||||
|
|
||||||
// Create the compositor.
|
// Create the compositor.
|
||||||
let mut event_loop = EventLoop::try_new().unwrap();
|
let mut event_loop = EventLoop::try_new().unwrap();
|
||||||
@@ -180,7 +175,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
event_loop.handle(),
|
event_loop.handle(),
|
||||||
event_loop.get_signal(),
|
event_loop.get_signal(),
|
||||||
display,
|
display,
|
||||||
);
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Set WAYLAND_DISPLAY for children.
|
// Set WAYLAND_DISPLAY for children.
|
||||||
let socket_name = &state.niri.socket_name;
|
let socket_name = &state.niri.socket_name;
|
||||||
@@ -196,9 +192,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_systemd_service {
|
if cli.session {
|
||||||
// We're starting as a systemd service. Export our variables.
|
// We're starting as a session. Import our variables.
|
||||||
import_env_to_systemd();
|
import_environment();
|
||||||
|
|
||||||
// Inhibit power key handling so we can suspend on it.
|
// Inhibit power key handling so we can suspend on it.
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
@@ -210,15 +206,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dbus")]
|
#[cfg(feature = "dbus")]
|
||||||
dbus::DBusServers::start(&mut state, is_systemd_service);
|
dbus::DBusServers::start(&mut state, cli.session);
|
||||||
|
|
||||||
// Notify systemd we're ready.
|
// Notify systemd we're ready.
|
||||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||||
warn!("error notifying systemd: {err:?}");
|
warn!("error notifying systemd: {err:?}");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Send ready notification to the NOTIFY_FD file descriptor.
|
||||||
|
if let Err(err) = notify_fd() {
|
||||||
|
warn!("error notifying fd: {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
// Set up config file watcher.
|
// Set up config file watcher.
|
||||||
let _watcher = if let Some(path) = path {
|
let _watcher = if let Some(path) = path.clone() {
|
||||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||||
let watcher = Watcher::new(path.clone(), tx);
|
let watcher = Watcher::new(path.clone(), tx);
|
||||||
event_loop
|
event_loop
|
||||||
@@ -243,6 +244,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Show the config error notification right away if needed.
|
// Show the config error notification right away if needed.
|
||||||
if config_errored {
|
if config_errored {
|
||||||
state.niri.config_error_notification.show();
|
state.niri.config_error_notification.show();
|
||||||
|
} else if config_created {
|
||||||
|
state.niri.config_error_notification.show_created(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the compositor.
|
// Run the compositor.
|
||||||
@@ -253,21 +256,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn version() -> String {
|
fn import_environment() {
|
||||||
format!(
|
let variables = [
|
||||||
"{} ({})",
|
"WAYLAND_DISPLAY",
|
||||||
env!("CARGO_PKG_VERSION"),
|
"XDG_CURRENT_DESKTOP",
|
||||||
git_version!(fallback = "unknown commit"),
|
"XDG_SESSION_TYPE",
|
||||||
)
|
niri_ipc::SOCKET_PATH_ENV,
|
||||||
}
|
]
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
let mut init_system_import = String::new();
|
||||||
|
if cfg!(feature = "systemd") {
|
||||||
|
write!(
|
||||||
|
init_system_import,
|
||||||
|
"systemctl --user import-environment {variables};"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
if cfg!(feature = "dinit") {
|
||||||
|
write!(init_system_import, "dinitctl setenv {variables};").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn import_env_to_systemd() {
|
|
||||||
let rv = Command::new("/bin/sh")
|
let rv = Command::new("/bin/sh")
|
||||||
.args([
|
.args([
|
||||||
"-c",
|
"-c",
|
||||||
"systemctl --user import-environment WAYLAND_DISPLAY && \
|
&format!(
|
||||||
hash dbus-update-activation-environment 2>/dev/null && \
|
"{init_system_import}\
|
||||||
dbus-update-activation-environment WAYLAND_DISPLAY",
|
hash dbus-update-activation-environment 2>/dev/null && \
|
||||||
|
dbus-update-activation-environment {variables}"
|
||||||
|
),
|
||||||
])
|
])
|
||||||
.spawn();
|
.spawn();
|
||||||
// Wait for the import process to complete, otherwise services will start too fast without
|
// Wait for the import process to complete, otherwise services will start too fast without
|
||||||
@@ -284,7 +301,7 @@ fn import_env_to_systemd() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("error spawning shell to import environment into systemd: {err:?}");
|
warn!("error spawning shell to import environment: {err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,3 +316,15 @@ fn default_config_path() -> Option<PathBuf> {
|
|||||||
path.push("config.kdl");
|
path.push("config.kdl");
|
||||||
Some(path)
|
Some(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn notify_fd() -> anyhow::Result<()> {
|
||||||
|
let fd = match env::var("NOTIFY_FD") {
|
||||||
|
Ok(notify_fd) => notify_fd.parse()?,
|
||||||
|
Err(env::VarError::NotPresent) => return Ok(()),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
env::remove_var("NOTIFY_FD");
|
||||||
|
let mut notif = unsafe { File::from_raw_fd(fd) };
|
||||||
|
notif.write_all(b"READY=1\n")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
+1158
-682
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,466 @@
|
|||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use arrayvec::ArrayVec;
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||||
|
use smithay::reexports::wayland_protocols_wlr;
|
||||||
|
use smithay::reexports::wayland_server::backend::ClientId;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
use smithay::reexports::wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
|
};
|
||||||
|
use smithay::wayland::compositor::with_states;
|
||||||
|
use smithay::wayland::shell::xdg::{
|
||||||
|
ToplevelStateSet, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||||
|
};
|
||||||
|
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||||
|
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||||
|
};
|
||||||
|
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||||
|
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||||
|
|
||||||
|
use crate::niri::State;
|
||||||
|
|
||||||
|
const VERSION: u32 = 3;
|
||||||
|
|
||||||
|
pub struct ForeignToplevelManagerState {
|
||||||
|
display: DisplayHandle,
|
||||||
|
instances: Vec<ZwlrForeignToplevelManagerV1>,
|
||||||
|
toplevels: HashMap<WlSurface, ToplevelData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ForeignToplevelHandler {
|
||||||
|
fn foreign_toplevel_manager_state(&mut self) -> &mut ForeignToplevelManagerState;
|
||||||
|
fn activate(&mut self, wl_surface: WlSurface);
|
||||||
|
fn close(&mut self, wl_surface: WlSurface);
|
||||||
|
fn set_fullscreen(&mut self, wl_surface: WlSurface, wl_output: Option<WlOutput>);
|
||||||
|
fn unset_fullscreen(&mut self, wl_surface: WlSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ToplevelData {
|
||||||
|
title: Option<String>,
|
||||||
|
app_id: Option<String>,
|
||||||
|
states: ArrayVec<u32, 3>,
|
||||||
|
output: Option<Output>,
|
||||||
|
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
|
||||||
|
// FIXME: parent.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ForeignToplevelGlobalData {
|
||||||
|
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ForeignToplevelManagerState {
|
||||||
|
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||||
|
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||||
|
D: 'static,
|
||||||
|
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let global_data = ForeignToplevelGlobalData {
|
||||||
|
filter: Box::new(filter),
|
||||||
|
};
|
||||||
|
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
|
||||||
|
Self {
|
||||||
|
display: display.clone(),
|
||||||
|
instances: Vec::new(),
|
||||||
|
toplevels: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh(state: &mut State) {
|
||||||
|
let _span = tracy_client::span!("foreign_toplevel::refresh");
|
||||||
|
|
||||||
|
let protocol_state = &mut state.niri.foreign_toplevel_state;
|
||||||
|
|
||||||
|
// Handle closed windows.
|
||||||
|
protocol_state.toplevels.retain(|surface, data| {
|
||||||
|
if state.niri.layout.find_window_and_output(surface).is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for instance in data.instances.keys() {
|
||||||
|
instance.closed();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle new and existing windows.
|
||||||
|
//
|
||||||
|
// Save the focused window for last, this way when the focus changes, we will first deactivate
|
||||||
|
// the previous window and only then activate the newly focused window.
|
||||||
|
let mut focused = None;
|
||||||
|
state.niri.layout.with_windows(|mapped, output| {
|
||||||
|
let wl_surface = mapped.toplevel().wl_surface();
|
||||||
|
|
||||||
|
with_states(wl_surface, |states| {
|
||||||
|
let role = states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
|
||||||
|
focused = Some((mapped.window.clone(), output.cloned()));
|
||||||
|
} else {
|
||||||
|
refresh_toplevel(protocol_state, wl_surface, &role, output, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finally, refresh the focused window.
|
||||||
|
if let Some((window, output)) = focused {
|
||||||
|
let wl_surface = window.toplevel().expect("no x11 support").wl_surface();
|
||||||
|
|
||||||
|
with_states(wl_surface, |states| {
|
||||||
|
let role = states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
refresh_toplevel(protocol_state, wl_surface, &role, output.as_ref(), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) {
|
||||||
|
let _span = tracy_client::span!("foreign_toplevel::on_output_bound");
|
||||||
|
|
||||||
|
let Some(client) = wl_output.client() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let protocol_state = &mut state.niri.foreign_toplevel_state;
|
||||||
|
for data in protocol_state.toplevels.values_mut() {
|
||||||
|
if data.output.as_ref() != Some(output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (instance, outputs) in &mut data.instances {
|
||||||
|
if instance.client().as_ref() != Some(&client) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.output_enter(wl_output);
|
||||||
|
instance.done();
|
||||||
|
outputs.push(wl_output.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_toplevel(
|
||||||
|
protocol_state: &mut ForeignToplevelManagerState,
|
||||||
|
wl_surface: &WlSurface,
|
||||||
|
role: &XdgToplevelSurfaceRoleAttributes,
|
||||||
|
output: Option<&Output>,
|
||||||
|
has_focus: bool,
|
||||||
|
) {
|
||||||
|
let states = to_state_vec(&role.current.states, has_focus);
|
||||||
|
|
||||||
|
match protocol_state.toplevels.entry(wl_surface.clone()) {
|
||||||
|
Entry::Occupied(entry) => {
|
||||||
|
// Existing window, check if anything changed.
|
||||||
|
let data = entry.into_mut();
|
||||||
|
|
||||||
|
let mut new_title = None;
|
||||||
|
if data.title != role.title {
|
||||||
|
data.title.clone_from(&role.title);
|
||||||
|
new_title = role.title.as_deref();
|
||||||
|
|
||||||
|
if new_title.is_none() {
|
||||||
|
error!("toplevel title changed to None");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_app_id = None;
|
||||||
|
if data.app_id != role.app_id {
|
||||||
|
data.app_id.clone_from(&role.app_id);
|
||||||
|
new_app_id = role.app_id.as_deref();
|
||||||
|
|
||||||
|
if new_app_id.is_none() {
|
||||||
|
error!("toplevel app_id changed to None");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut states_changed = false;
|
||||||
|
if data.states != states {
|
||||||
|
data.states = states;
|
||||||
|
states_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output_changed = false;
|
||||||
|
if data.output.as_ref() != output {
|
||||||
|
data.output = output.cloned();
|
||||||
|
output_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let something_changed =
|
||||||
|
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
|
||||||
|
|
||||||
|
if something_changed {
|
||||||
|
for (instance, outputs) in &mut data.instances {
|
||||||
|
if let Some(new_title) = new_title {
|
||||||
|
instance.title(new_title.to_owned());
|
||||||
|
}
|
||||||
|
if let Some(new_app_id) = new_app_id {
|
||||||
|
instance.app_id(new_app_id.to_owned());
|
||||||
|
}
|
||||||
|
if states_changed {
|
||||||
|
instance.state(data.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
|
||||||
|
}
|
||||||
|
if output_changed {
|
||||||
|
for wl_output in outputs.drain(..) {
|
||||||
|
instance.output_leave(&wl_output);
|
||||||
|
}
|
||||||
|
if let Some(output) = &data.output {
|
||||||
|
if let Some(client) = instance.client() {
|
||||||
|
for wl_output in output.client_outputs(&client) {
|
||||||
|
instance.output_enter(&wl_output);
|
||||||
|
outputs.push(wl_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instance.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for outputs in data.instances.values_mut() {
|
||||||
|
// Clean up dead wl_outputs.
|
||||||
|
outputs.retain(|x| x.is_alive());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
// New window, start tracking it.
|
||||||
|
let mut data = ToplevelData {
|
||||||
|
title: role.title.clone(),
|
||||||
|
app_id: role.app_id.clone(),
|
||||||
|
states,
|
||||||
|
output: output.cloned(),
|
||||||
|
instances: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for manager in &protocol_state.instances {
|
||||||
|
if let Some(client) = manager.client() {
|
||||||
|
data.add_instance::<State>(&protocol_state.display, &client, manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.insert(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToplevelData {
|
||||||
|
fn add_instance<D>(
|
||||||
|
&mut self,
|
||||||
|
handle: &DisplayHandle,
|
||||||
|
client: &Client,
|
||||||
|
manager: &ZwlrForeignToplevelManagerV1,
|
||||||
|
) where
|
||||||
|
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
let toplevel = client
|
||||||
|
.create_resource::<ZwlrForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
|
||||||
|
.unwrap();
|
||||||
|
manager.toplevel(&toplevel);
|
||||||
|
|
||||||
|
if let Some(title) = &self.title {
|
||||||
|
toplevel.title(title.clone());
|
||||||
|
}
|
||||||
|
if let Some(app_id) = &self.app_id {
|
||||||
|
toplevel.app_id(app_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
toplevel.state(self.states.iter().flat_map(|x| x.to_ne_bytes()).collect());
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
if let Some(output) = &self.output {
|
||||||
|
for wl_output in output.client_outputs(client) {
|
||||||
|
toplevel.output_enter(&wl_output);
|
||||||
|
outputs.push(wl_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toplevel.done();
|
||||||
|
|
||||||
|
self.instances.insert(toplevel, outputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData, D>
|
||||||
|
for ForeignToplevelManagerState
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
|
||||||
|
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||||
|
D: ForeignToplevelHandler,
|
||||||
|
{
|
||||||
|
fn bind(
|
||||||
|
state: &mut D,
|
||||||
|
handle: &DisplayHandle,
|
||||||
|
client: &Client,
|
||||||
|
resource: New<ZwlrForeignToplevelManagerV1>,
|
||||||
|
_global_data: &ForeignToplevelGlobalData,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
let manager = data_init.init(resource, ());
|
||||||
|
|
||||||
|
let state = state.foreign_toplevel_manager_state();
|
||||||
|
|
||||||
|
for data in state.toplevels.values_mut() {
|
||||||
|
data.add_instance::<D>(handle, client, &manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.instances.push(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
|
||||||
|
(global_data.filter)(&client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrForeignToplevelManagerV1, (), D> for ForeignToplevelManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||||
|
D: ForeignToplevelHandler,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZwlrForeignToplevelManagerV1,
|
||||||
|
request: <ZwlrForeignToplevelManagerV1 as Resource>::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
|
||||||
|
resource.finished();
|
||||||
|
|
||||||
|
let state = state.foreign_toplevel_manager_state();
|
||||||
|
state.instances.retain(|x| x != resource);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyed(
|
||||||
|
state: &mut D,
|
||||||
|
_client: ClientId,
|
||||||
|
resource: &ZwlrForeignToplevelManagerV1,
|
||||||
|
_data: &(),
|
||||||
|
) {
|
||||||
|
let state = state.foreign_toplevel_manager_state();
|
||||||
|
state.instances.retain(|x| x != resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrForeignToplevelHandleV1, ()>,
|
||||||
|
D: ForeignToplevelHandler,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZwlrForeignToplevelHandleV1,
|
||||||
|
request: <ZwlrForeignToplevelHandleV1 as Resource>::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
let protocol_state = state.foreign_toplevel_manager_state();
|
||||||
|
|
||||||
|
let Some((surface, _)) = protocol_state
|
||||||
|
.toplevels
|
||||||
|
.iter()
|
||||||
|
.find(|(_, data)| data.instances.contains_key(resource))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let surface = surface.clone();
|
||||||
|
|
||||||
|
match request {
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => (),
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => (),
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => (),
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => (),
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::Activate { .. } => {
|
||||||
|
state.activate(surface);
|
||||||
|
}
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::Close => {
|
||||||
|
state.close(surface);
|
||||||
|
}
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { .. } => (),
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::Destroy => (),
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => {
|
||||||
|
state.set_fullscreen(surface, output);
|
||||||
|
}
|
||||||
|
zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => {
|
||||||
|
state.unset_fullscreen(surface);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyed(
|
||||||
|
state: &mut D,
|
||||||
|
_client: ClientId,
|
||||||
|
resource: &ZwlrForeignToplevelHandleV1,
|
||||||
|
_data: &(),
|
||||||
|
) {
|
||||||
|
let state = state.foreign_toplevel_manager_state();
|
||||||
|
for data in state.toplevels.values_mut() {
|
||||||
|
data.instances.retain(|instance, _| instance != resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3> {
|
||||||
|
let mut rv = ArrayVec::new();
|
||||||
|
if states.contains(xdg_toplevel::State::Maximized) {
|
||||||
|
rv.push(zwlr_foreign_toplevel_handle_v1::State::Maximized as u32);
|
||||||
|
}
|
||||||
|
if states.contains(xdg_toplevel::State::Fullscreen) {
|
||||||
|
rv.push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: wlr-foreign-toplevel-management states:
|
||||||
|
//
|
||||||
|
// These have the same meaning as the states with the same names defined in xdg-toplevel
|
||||||
|
//
|
||||||
|
// However, clients such as sfwbar and fcitx seem to treat the activated state as keyboard
|
||||||
|
// focus, i.e. they don't expect multiple windows to have it set at once. Even Waybar which
|
||||||
|
// handles multiple activated windows correctly uses it in its design in such a way that
|
||||||
|
// keyboard focus would make more sense. Let's do what the clients expect.
|
||||||
|
if has_focus {
|
||||||
|
rv.push(zwlr_foreign_toplevel_handle_v1::State::Activated as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
rv
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! delegate_foreign_toplevel {
|
||||||
|
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||||
|
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
|
||||||
|
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: ()
|
||||||
|
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: ()
|
||||||
|
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_protocols_wlr;
|
||||||
|
use smithay::reexports::wayland_server::backend::ClientId;
|
||||||
|
use smithay::reexports::wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
|
};
|
||||||
|
use wayland_protocols_wlr::gamma_control::v1::server::{
|
||||||
|
zwlr_gamma_control_manager_v1, zwlr_gamma_control_v1,
|
||||||
|
};
|
||||||
|
use zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1;
|
||||||
|
use zwlr_gamma_control_v1::ZwlrGammaControlV1;
|
||||||
|
|
||||||
|
const VERSION: u32 = 1;
|
||||||
|
|
||||||
|
pub struct GammaControlManagerState {
|
||||||
|
// Active gamma controls only. Failed ones are removed.
|
||||||
|
gamma_controls: HashMap<Output, ZwlrGammaControlV1>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GammaControlManagerGlobalData {
|
||||||
|
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait GammaControlHandler {
|
||||||
|
fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState;
|
||||||
|
fn get_gamma_size(&mut self, output: &Output) -> Option<u32>;
|
||||||
|
fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> Option<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GammaControlState {
|
||||||
|
gamma_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GammaControlManagerState {
|
||||||
|
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
|
||||||
|
D: GammaControlHandler,
|
||||||
|
D: 'static,
|
||||||
|
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let global_data = GammaControlManagerGlobalData {
|
||||||
|
filter: Box::new(filter),
|
||||||
|
};
|
||||||
|
display.create_global::<D, ZwlrGammaControlManagerV1, _>(VERSION, global_data);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
gamma_controls: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_removed(&mut self, output: &Output) {
|
||||||
|
if let Some(gamma_control) = self.gamma_controls.remove(output) {
|
||||||
|
gamma_control.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData, D>
|
||||||
|
for GammaControlManagerState
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
|
||||||
|
D: GammaControlHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn bind(
|
||||||
|
_state: &mut D,
|
||||||
|
_handle: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
manager: New<ZwlrGammaControlManagerV1>,
|
||||||
|
_manager_state: &GammaControlManagerGlobalData,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
data_init.init(manager, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_view(client: Client, global_data: &GammaControlManagerGlobalData) -> bool {
|
||||||
|
(global_data.filter)(&client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrGammaControlManagerV1, (), D> for GammaControlManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrGammaControlManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
|
||||||
|
D: GammaControlHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
_resource: &ZwlrGammaControlManagerV1,
|
||||||
|
request: <ZwlrGammaControlManagerV1 as Resource>::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_gamma_control_manager_v1::Request::GetGammaControl { id, output } => {
|
||||||
|
if let Some(output) = Output::from_resource(&output) {
|
||||||
|
// We borrow state in the middle.
|
||||||
|
#[allow(clippy::map_entry)]
|
||||||
|
if !state
|
||||||
|
.gamma_control_manager_state()
|
||||||
|
.gamma_controls
|
||||||
|
.contains_key(&output)
|
||||||
|
{
|
||||||
|
if let Some(gamma_size) = state.get_gamma_size(&output) {
|
||||||
|
let zwlr_gamma_control =
|
||||||
|
data_init.init(id, GammaControlState { gamma_size });
|
||||||
|
zwlr_gamma_control.gamma_size(gamma_size);
|
||||||
|
state
|
||||||
|
.gamma_control_manager_state()
|
||||||
|
.gamma_controls
|
||||||
|
.insert(output, zwlr_gamma_control);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data_init
|
||||||
|
.init(id, GammaControlState { gamma_size: 0 })
|
||||||
|
.failed();
|
||||||
|
}
|
||||||
|
zwlr_gamma_control_manager_v1::Request::Destroy => (),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrGammaControlV1, GammaControlState, D> for GammaControlManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrGammaControlV1, GammaControlState>,
|
||||||
|
D: GammaControlHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZwlrGammaControlV1,
|
||||||
|
request: <ZwlrGammaControlV1 as Resource>::Request,
|
||||||
|
data: &GammaControlState,
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_gamma_control_v1::Request::SetGamma { fd } => {
|
||||||
|
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
|
||||||
|
let Some((output, _)) = gamma_controls.iter().find(|(_, x)| *x == resource) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let output = output.clone();
|
||||||
|
|
||||||
|
trace!("setting gamma for output {}", output.name());
|
||||||
|
|
||||||
|
// Start with a u16 slice so it's aligned correctly.
|
||||||
|
let mut gamma = vec![0u16; data.gamma_size as usize * 3];
|
||||||
|
let buf = bytemuck::cast_slice_mut(&mut gamma);
|
||||||
|
let mut file = File::from(fd);
|
||||||
|
{
|
||||||
|
let _span = tracy_client::span!("read gamma from fd");
|
||||||
|
|
||||||
|
if let Err(err) = file.read_exact(buf) {
|
||||||
|
warn!("failed to read gamma data: {err:?}");
|
||||||
|
resource.failed();
|
||||||
|
gamma_controls.remove(&output);
|
||||||
|
let _ = state.set_gamma(&output, None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that there's no more data.
|
||||||
|
#[allow(clippy::unused_io_amount)] // False positive on 1.77.0
|
||||||
|
{
|
||||||
|
match file.read(&mut [0]) {
|
||||||
|
Ok(0) => (),
|
||||||
|
Ok(_) => {
|
||||||
|
warn!("gamma data is too large");
|
||||||
|
resource.failed();
|
||||||
|
gamma_controls.remove(&output);
|
||||||
|
let _ = state.set_gamma(&output, None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error reading gamma data: {err:?}");
|
||||||
|
resource.failed();
|
||||||
|
gamma_controls.remove(&output);
|
||||||
|
let _ = state.set_gamma(&output, None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.set_gamma(&output, Some(gamma)).is_none() {
|
||||||
|
resource.failed();
|
||||||
|
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
|
||||||
|
gamma_controls.remove(&output);
|
||||||
|
let _ = state.set_gamma(&output, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zwlr_gamma_control_v1::Request::Destroy => (),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyed(
|
||||||
|
state: &mut D,
|
||||||
|
_client: ClientId,
|
||||||
|
resource: &ZwlrGammaControlV1,
|
||||||
|
_data: &GammaControlState,
|
||||||
|
) {
|
||||||
|
let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls;
|
||||||
|
let Some((output, _)) = gamma_controls.iter().find(|(_, x)| *x == resource) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let output = output.clone();
|
||||||
|
gamma_controls.remove(&output);
|
||||||
|
|
||||||
|
let _ = state.set_gamma(&output, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! delegate_gamma_control {
|
||||||
|
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||||
|
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: $crate::protocols::gamma_control::GammaControlManagerGlobalData
|
||||||
|
] => $crate::protocols::gamma_control::GammaControlManagerState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: ()
|
||||||
|
] => $crate::protocols::gamma_control::GammaControlManagerState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_v1::ZwlrGammaControlV1: $crate::protocols::gamma_control::GammaControlState
|
||||||
|
] => $crate::protocols::gamma_control::GammaControlManagerState);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod foreign_toplevel;
|
||||||
|
pub mod gamma_control;
|
||||||
|
pub mod screencopy;
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||||
|
Flags, ZwlrScreencopyFrameV1,
|
||||||
|
};
|
||||||
|
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||||
|
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||||
|
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
|
||||||
|
};
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||||
|
use smithay::reexports::wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
|
};
|
||||||
|
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||||
|
use smithay::wayland::shm;
|
||||||
|
|
||||||
|
// We do not support copy_with_damage() semantics yet.
|
||||||
|
const VERSION: u32 = 1;
|
||||||
|
|
||||||
|
pub struct ScreencopyManagerState;
|
||||||
|
|
||||||
|
pub struct ScreencopyManagerGlobalData {
|
||||||
|
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreencopyManagerState {
|
||||||
|
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||||
|
D: ScreencopyHandler,
|
||||||
|
D: 'static,
|
||||||
|
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let global_data = ScreencopyManagerGlobalData {
|
||||||
|
filter: Box::new(filter),
|
||||||
|
};
|
||||||
|
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
|
||||||
|
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData, D>
|
||||||
|
for ScreencopyManagerState
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||||
|
D: ScreencopyHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn bind(
|
||||||
|
_state: &mut D,
|
||||||
|
_display: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
manager: New<ZwlrScreencopyManagerV1>,
|
||||||
|
_manager_state: &ScreencopyManagerGlobalData,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
data_init.init(manager, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
|
||||||
|
(global_data.filter)(&client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrScreencopyManagerV1, (), D> for ScreencopyManagerState
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrScreencopyManagerV1, ScreencopyManagerGlobalData>,
|
||||||
|
D: Dispatch<ZwlrScreencopyManagerV1, ()>,
|
||||||
|
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||||
|
D: ScreencopyHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
_state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
_manager: &ZwlrScreencopyManagerV1,
|
||||||
|
request: zwlr_screencopy_manager_v1::Request,
|
||||||
|
_data: &(),
|
||||||
|
_display: &DisplayHandle,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
let (frame, overlay_cursor, buffer_size, region_loc, output) = match request {
|
||||||
|
zwlr_screencopy_manager_v1::Request::CaptureOutput {
|
||||||
|
frame,
|
||||||
|
overlay_cursor,
|
||||||
|
output,
|
||||||
|
} => {
|
||||||
|
let output = Output::from_resource(&output).unwrap();
|
||||||
|
let buffer_size = output.current_mode().unwrap().size;
|
||||||
|
let region_loc = Point::from((0, 0));
|
||||||
|
|
||||||
|
(frame, overlay_cursor, buffer_size, region_loc, output)
|
||||||
|
}
|
||||||
|
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
||||||
|
frame,
|
||||||
|
overlay_cursor,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
output,
|
||||||
|
} => {
|
||||||
|
if width <= 0 || height <= 0 {
|
||||||
|
trace!("screencopy client requested invalid sized region");
|
||||||
|
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
|
||||||
|
frame.failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Output::from_resource(&output).unwrap();
|
||||||
|
let output_transform = output.current_transform();
|
||||||
|
let output_physical_size =
|
||||||
|
output_transform.transform_size(output.current_mode().unwrap().size);
|
||||||
|
let output_rect = Rectangle::from_loc_and_size((0, 0), output_physical_size);
|
||||||
|
|
||||||
|
let rect = Rectangle::from_loc_and_size((x, y), (width, height));
|
||||||
|
|
||||||
|
let output_scale = output.current_scale().integer_scale();
|
||||||
|
let physical_rect = rect.to_physical(output_scale);
|
||||||
|
|
||||||
|
// Clamp captured region to the output.
|
||||||
|
let Some(clamped_rect) = physical_rect.intersection(output_rect) else {
|
||||||
|
trace!("screencopy client requested region outside of output");
|
||||||
|
let frame = data_init.init(frame, ScreencopyFrameState::Failed);
|
||||||
|
frame.failed();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let untransformed_rect = output_transform
|
||||||
|
.invert()
|
||||||
|
.transform_rect_in(clamped_rect, &output_physical_size);
|
||||||
|
|
||||||
|
(
|
||||||
|
frame,
|
||||||
|
overlay_cursor,
|
||||||
|
untransformed_rect.size,
|
||||||
|
clamped_rect.loc,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
zwlr_screencopy_manager_v1::Request::Destroy => return,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the frame.
|
||||||
|
let overlay_cursor = overlay_cursor != 0;
|
||||||
|
let info = ScreencopyFrameInfo {
|
||||||
|
output,
|
||||||
|
overlay_cursor,
|
||||||
|
buffer_size,
|
||||||
|
region_loc,
|
||||||
|
};
|
||||||
|
let frame = data_init.init(
|
||||||
|
frame,
|
||||||
|
ScreencopyFrameState::Pending {
|
||||||
|
info,
|
||||||
|
copied: Arc::new(AtomicBool::new(false)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send desired SHM buffer parameters.
|
||||||
|
frame.buffer(
|
||||||
|
wl_shm::Format::Argb8888,
|
||||||
|
buffer_size.w as u32,
|
||||||
|
buffer_size.h as u32,
|
||||||
|
buffer_size.w as u32 * 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
// if manager.version() >= 3 {
|
||||||
|
// // Send desired DMA buffer parameters.
|
||||||
|
// frame.linux_dmabuf(
|
||||||
|
// Fourcc::Argb8888 as u32,
|
||||||
|
// buffer_size.w as u32,
|
||||||
|
// buffer_size.h as u32,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// // Notify client that all supported buffers were enumerated.
|
||||||
|
// frame.buffer_done();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler trait for wlr-screencopy.
|
||||||
|
pub trait ScreencopyHandler {
|
||||||
|
/// Handle new screencopy request.
|
||||||
|
fn frame(&mut self, frame: Screencopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! delegate_screencopy {
|
||||||
|
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||||
|
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: $crate::protocols::screencopy::ScreencopyManagerGlobalData
|
||||||
|
] => $crate::protocols::screencopy::ScreencopyManagerState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: ()
|
||||||
|
] => $crate::protocols::screencopy::ScreencopyManagerState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1: $crate::protocols::screencopy::ScreencopyFrameState
|
||||||
|
] => $crate::protocols::screencopy::ScreencopyManagerState);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ScreencopyFrameInfo {
|
||||||
|
output: Output,
|
||||||
|
buffer_size: Size<i32, Physical>,
|
||||||
|
region_loc: Point<i32, Physical>,
|
||||||
|
overlay_cursor: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ScreencopyFrameState {
|
||||||
|
Failed,
|
||||||
|
Pending {
|
||||||
|
info: ScreencopyFrameInfo,
|
||||||
|
copied: Arc<AtomicBool>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState, D> for ScreencopyManagerState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState>,
|
||||||
|
D: ScreencopyHandler,
|
||||||
|
D: 'static,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
frame: &ZwlrScreencopyFrameV1,
|
||||||
|
request: zwlr_screencopy_frame_v1::Request,
|
||||||
|
data: &ScreencopyFrameState,
|
||||||
|
_display: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
if matches!(request, zwlr_screencopy_frame_v1::Request::Destroy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (info, copied) = match data {
|
||||||
|
ScreencopyFrameState::Failed => return,
|
||||||
|
ScreencopyFrameState::Pending { info, copied } => (info, copied),
|
||||||
|
};
|
||||||
|
|
||||||
|
if copied.load(Ordering::SeqCst) {
|
||||||
|
frame.post_error(
|
||||||
|
zwlr_screencopy_frame_v1::Error::AlreadyUsed,
|
||||||
|
"copy was already requested",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (buffer, with_damage) = match request {
|
||||||
|
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
|
||||||
|
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !shm::with_buffer_contents(&buffer, |_buf, shm_len, buffer_data| {
|
||||||
|
buffer_data.format == wl_shm::Format::Argb8888
|
||||||
|
&& buffer_data.stride == info.buffer_size.w * 4
|
||||||
|
&& buffer_data.height == info.buffer_size.h
|
||||||
|
&& shm_len as i32 == buffer_data.stride * buffer_data.height
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
frame.post_error(
|
||||||
|
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
|
||||||
|
"invalid buffer",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copied.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
state.frame(Screencopy {
|
||||||
|
with_damage,
|
||||||
|
buffer,
|
||||||
|
frame: frame.clone(),
|
||||||
|
info: info.clone(),
|
||||||
|
submitted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Screencopy frame.
|
||||||
|
pub struct Screencopy {
|
||||||
|
info: ScreencopyFrameInfo,
|
||||||
|
frame: ZwlrScreencopyFrameV1,
|
||||||
|
#[allow(unused)]
|
||||||
|
with_damage: bool,
|
||||||
|
buffer: WlBuffer,
|
||||||
|
submitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Screencopy {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.submitted {
|
||||||
|
self.frame.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Screencopy {
|
||||||
|
/// Get the target buffer to copy to.
|
||||||
|
pub fn buffer(&self) -> &WlBuffer {
|
||||||
|
&self.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn region_loc(&self) -> Point<i32, Physical> {
|
||||||
|
self.info.region_loc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_size(&self) -> Size<i32, Physical> {
|
||||||
|
self.info.buffer_size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output(&self) -> &Output {
|
||||||
|
&self.info.output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn overlay_cursor(&self) -> bool {
|
||||||
|
self.info.overlay_cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn damage(&mut self, damage: &[Rectangle<i32, Physical>]) {
|
||||||
|
// assert!(self.with_damage);
|
||||||
|
//
|
||||||
|
// for Rectangle { loc, size } in damage {
|
||||||
|
// self.frame
|
||||||
|
// .damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Submit the copied content.
|
||||||
|
pub fn submit(mut self, y_invert: bool) {
|
||||||
|
// Notify client that buffer is ordinary.
|
||||||
|
self.frame.flags(if y_invert {
|
||||||
|
Flags::YInvert
|
||||||
|
} else {
|
||||||
|
Flags::empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify client about successful copy.
|
||||||
|
let time = UNIX_EPOCH.elapsed().unwrap();
|
||||||
|
let tv_sec_hi = (time.as_secs() >> 32) as u32;
|
||||||
|
let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32;
|
||||||
|
let tv_nsec = time.subsec_nanos();
|
||||||
|
self.frame.ready(tv_sec_hi, tv_sec_lo, tv_nsec);
|
||||||
|
|
||||||
|
// Mark frame as submitted to ensure destructor isn't run.
|
||||||
|
self.submitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn submit_after_sync<T>(
|
||||||
|
// self,
|
||||||
|
// y_invert: bool,
|
||||||
|
// sync_point: Option<OwnedFd>,
|
||||||
|
// event_loop: &LoopHandle<'_, T>,
|
||||||
|
// ) {
|
||||||
|
// match sync_point {
|
||||||
|
// None => self.submit(y_invert),
|
||||||
|
// Some(sync_fd) => {
|
||||||
|
// let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
|
||||||
|
// let mut screencopy = Some(self);
|
||||||
|
// event_loop
|
||||||
|
// .insert_source(source, move |_, _, _| {
|
||||||
|
// screencopy.take().unwrap().submit(y_invert);
|
||||||
|
// Ok(PostAction::Remove)
|
||||||
|
// })
|
||||||
|
// .unwrap();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
+25
-19
@@ -7,42 +7,48 @@ use std::rc::Rc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use pipewire::spa::data::DataType;
|
use pipewire::context::Context;
|
||||||
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
|
use pipewire::core::Core;
|
||||||
|
use pipewire::main_loop::MainLoop;
|
||||||
|
use pipewire::properties::Properties;
|
||||||
|
use pipewire::spa::buffer::DataType;
|
||||||
|
use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
|
||||||
use pipewire::spa::param::format_utils::parse_format;
|
use pipewire::spa::param::format_utils::parse_format;
|
||||||
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
|
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
|
||||||
use pipewire::spa::param::ParamType;
|
use pipewire::spa::param::ParamType;
|
||||||
use pipewire::spa::pod::serialize::PodSerializer;
|
use pipewire::spa::pod::serialize::PodSerializer;
|
||||||
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
|
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
|
||||||
use pipewire::spa::sys::*;
|
use pipewire::spa::sys::*;
|
||||||
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
|
use pipewire::spa::utils::{
|
||||||
use pipewire::spa::Direction;
|
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
|
||||||
|
};
|
||||||
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
|
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
|
||||||
use pipewire::{Context, Core, MainLoop, Properties};
|
|
||||||
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
||||||
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
|
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::drm::DrmDeviceFd;
|
use smithay::backend::drm::DrmDeviceFd;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::calloop::generic::Generic;
|
use smithay::reexports::calloop::generic::Generic;
|
||||||
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
|
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||||
use smithay::reexports::gbm::Modifier;
|
use smithay::reexports::gbm::Modifier;
|
||||||
|
use smithay::utils::{Physical, Size};
|
||||||
use zbus::SignalContext;
|
use zbus::SignalContext;
|
||||||
|
|
||||||
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
|
use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri};
|
||||||
use crate::niri::State;
|
use crate::niri::State;
|
||||||
|
|
||||||
pub struct PipeWire {
|
pub struct PipeWire {
|
||||||
_context: Context<MainLoop>,
|
_context: Context,
|
||||||
pub core: Core,
|
pub core: Core,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Cast {
|
pub struct Cast {
|
||||||
pub session_id: usize,
|
pub session_id: usize,
|
||||||
pub stream: Rc<Stream>,
|
pub stream: Stream,
|
||||||
_listener: StreamListener<()>,
|
_listener: StreamListener<()>,
|
||||||
pub is_active: Rc<Cell<bool>>,
|
pub is_active: Rc<Cell<bool>>,
|
||||||
pub output: Output,
|
pub output: Output,
|
||||||
|
pub size: Size<i32, Physical>,
|
||||||
pub cursor_mode: CursorMode,
|
pub cursor_mode: CursorMode,
|
||||||
pub last_frame_time: Duration,
|
pub last_frame_time: Duration,
|
||||||
pub min_time_between_frames: Rc<Cell<Duration>>,
|
pub min_time_between_frames: Rc<Cell<Duration>>,
|
||||||
@@ -51,7 +57,7 @@ pub struct Cast {
|
|||||||
|
|
||||||
impl PipeWire {
|
impl PipeWire {
|
||||||
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
|
||||||
let main_loop = MainLoop::new().context("error creating MainLoop")?;
|
let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
|
||||||
let context = Context::new(&main_loop).context("error creating Context")?;
|
let context = Context::new(&main_loop).context("error creating Context")?;
|
||||||
let core = context.connect(None).context("error creating Core")?;
|
let core = context.connect(None).context("error creating Core")?;
|
||||||
|
|
||||||
@@ -66,14 +72,14 @@ impl PipeWire {
|
|||||||
struct AsFdWrapper(MainLoop);
|
struct AsFdWrapper(MainLoop);
|
||||||
impl AsFd for AsFdWrapper {
|
impl AsFd for AsFdWrapper {
|
||||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||||
self.0.fd()
|
self.0.loop_().fd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
let generic = Generic::new(AsFdWrapper(main_loop), Interest::READ, Mode::Level);
|
||||||
event_loop
|
event_loop
|
||||||
.insert_source(generic, move |_, wrapper, _| {
|
.insert_source(generic, move |_, wrapper, _| {
|
||||||
let _span = tracy_client::span!("pipewire iteration");
|
let _span = tracy_client::span!("pipewire iteration");
|
||||||
wrapper.0.iterate(Duration::ZERO);
|
wrapper.0.loop_().iterate(Duration::ZERO);
|
||||||
Ok(PostAction::Continue)
|
Ok(PostAction::Continue)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -112,13 +118,14 @@ impl PipeWire {
|
|||||||
|
|
||||||
let mode = output.current_mode().unwrap();
|
let mode = output.current_mode().unwrap();
|
||||||
let size = mode.size;
|
let size = mode.size;
|
||||||
|
let transform = output.current_transform();
|
||||||
|
let size = transform.transform_size(size);
|
||||||
let refresh = mode.refresh;
|
let refresh = mode.refresh;
|
||||||
|
|
||||||
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
|
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
|
||||||
.context("error creating Stream")?;
|
.context("error creating Stream")?;
|
||||||
|
|
||||||
// Like in good old wayland-rs times...
|
// Like in good old wayland-rs times...
|
||||||
let stream = Rc::new(stream);
|
|
||||||
let node_id = Rc::new(Cell::new(None));
|
let node_id = Rc::new(Cell::new(None));
|
||||||
let is_active = Rc::new(Cell::new(false));
|
let is_active = Rc::new(Cell::new(false));
|
||||||
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
|
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
|
||||||
@@ -127,10 +134,9 @@ impl PipeWire {
|
|||||||
let listener = stream
|
let listener = stream
|
||||||
.add_local_listener_with_user_data(())
|
.add_local_listener_with_user_data(())
|
||||||
.state_changed({
|
.state_changed({
|
||||||
let stream = stream.clone();
|
|
||||||
let is_active = is_active.clone();
|
let is_active = is_active.clone();
|
||||||
let stop_cast = stop_cast.clone();
|
let stop_cast = stop_cast.clone();
|
||||||
move |old, new| {
|
move |stream, (), old, new| {
|
||||||
debug!("pw stream: state changed: {old:?} -> {new:?}");
|
debug!("pw stream: state changed: {old:?} -> {new:?}");
|
||||||
|
|
||||||
match new {
|
match new {
|
||||||
@@ -174,7 +180,7 @@ impl PipeWire {
|
|||||||
})
|
})
|
||||||
.param_changed({
|
.param_changed({
|
||||||
let min_time_between_frames = min_time_between_frames.clone();
|
let min_time_between_frames = min_time_between_frames.clone();
|
||||||
move |stream, id, _data, pod| {
|
move |stream, (), id, pod| {
|
||||||
let id = ParamType::from_raw(id);
|
let id = ParamType::from_raw(id);
|
||||||
trace!(?id, "pw stream: param_changed");
|
trace!(?id, "pw stream: param_changed");
|
||||||
|
|
||||||
@@ -256,8 +262,7 @@ impl PipeWire {
|
|||||||
let mut b1 = vec![];
|
let mut b1 = vec![];
|
||||||
// let mut b2 = vec![];
|
// let mut b2 = vec![];
|
||||||
let mut params = [
|
let mut params = [
|
||||||
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
|
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
|
||||||
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
|
|
||||||
];
|
];
|
||||||
stream.update_params(&mut params).unwrap();
|
stream.update_params(&mut params).unwrap();
|
||||||
}
|
}
|
||||||
@@ -265,7 +270,7 @@ impl PipeWire {
|
|||||||
.add_buffer({
|
.add_buffer({
|
||||||
let dmabufs = dmabufs.clone();
|
let dmabufs = dmabufs.clone();
|
||||||
let stop_cast = stop_cast.clone();
|
let stop_cast = stop_cast.clone();
|
||||||
move |buffer| {
|
move |_stream, (), buffer| {
|
||||||
trace!("pw stream: add_buffer");
|
trace!("pw stream: add_buffer");
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -309,7 +314,7 @@ impl PipeWire {
|
|||||||
})
|
})
|
||||||
.remove_buffer({
|
.remove_buffer({
|
||||||
let dmabufs = dmabufs.clone();
|
let dmabufs = dmabufs.clone();
|
||||||
move |buffer| {
|
move |_stream, (), buffer| {
|
||||||
trace!("pw stream: remove_buffer");
|
trace!("pw stream: remove_buffer");
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -383,6 +388,7 @@ impl PipeWire {
|
|||||||
_listener: listener,
|
_listener: listener,
|
||||||
is_active,
|
is_active,
|
||||||
output,
|
output,
|
||||||
|
size,
|
||||||
cursor_mode,
|
cursor_mode,
|
||||||
last_frame_time: Duration::ZERO,
|
last_frame_time: Duration::ZERO,
|
||||||
min_time_between_frames,
|
min_time_between_frames,
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
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};
|
||||||
|
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
|
||||||
|
|
||||||
|
use super::primary_gpu_pixel_shader_with_textures::PrimaryGpuPixelShaderWithTexturesRenderElement;
|
||||||
|
use super::renderer::AsGlesFrame;
|
||||||
|
use super::shaders::Shaders;
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CrossfadeRenderElement(PrimaryGpuPixelShaderWithTexturesRenderElement);
|
||||||
|
|
||||||
|
impl CrossfadeRenderElement {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
area: Rectangle<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
texture_from: (GlesTexture, Rectangle<i32, Physical>),
|
||||||
|
size_from: Size<i32, Logical>,
|
||||||
|
texture_to: (GlesTexture, Rectangle<i32, Physical>),
|
||||||
|
size_to: Size<i32, Logical>,
|
||||||
|
amount: f32,
|
||||||
|
result_alpha: f32,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let (texture_from, texture_from_geo) = texture_from;
|
||||||
|
let (texture_to, texture_to_geo) = texture_to;
|
||||||
|
|
||||||
|
let scale_from = area.size.to_f64() / size_from.to_f64();
|
||||||
|
let scale_to = area.size.to_f64() / size_to.to_f64();
|
||||||
|
|
||||||
|
let tex_from_geo = texture_from_geo.to_f64().upscale(scale_from);
|
||||||
|
let tex_to_geo = texture_to_geo.to_f64().upscale(scale_to);
|
||||||
|
let combined_geo = tex_from_geo.merge(tex_to_geo);
|
||||||
|
|
||||||
|
let size = combined_geo
|
||||||
|
.size
|
||||||
|
.to_logical(1.)
|
||||||
|
.to_buffer(1., Transform::Normal);
|
||||||
|
|
||||||
|
let area = Rectangle::from_loc_and_size(
|
||||||
|
area.loc + combined_geo.loc.to_logical(scale).to_i32_round(),
|
||||||
|
combined_geo.size.to_logical(scale).to_i32_round(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let tex_from_loc = (tex_from_geo.loc - combined_geo.loc)
|
||||||
|
.downscale((combined_geo.size.w, combined_geo.size.h));
|
||||||
|
let tex_to_loc = (tex_to_geo.loc - combined_geo.loc)
|
||||||
|
.downscale((combined_geo.size.w, combined_geo.size.h));
|
||||||
|
let tex_from_size = tex_from_geo.size / combined_geo.size;
|
||||||
|
let tex_to_size = tex_to_geo.size / combined_geo.size;
|
||||||
|
|
||||||
|
// FIXME: cropping this element will mess up the coordinates.
|
||||||
|
Shaders::get(renderer).crossfade.clone().map(|shader| {
|
||||||
|
Self(PrimaryGpuPixelShaderWithTexturesRenderElement::new(
|
||||||
|
shader,
|
||||||
|
HashMap::from([
|
||||||
|
(String::from("tex_from"), texture_from),
|
||||||
|
(String::from("tex_to"), texture_to),
|
||||||
|
]),
|
||||||
|
area,
|
||||||
|
size,
|
||||||
|
None,
|
||||||
|
result_alpha,
|
||||||
|
vec![
|
||||||
|
Uniform::new(
|
||||||
|
"tex_from_loc",
|
||||||
|
(tex_from_loc.x as f32, tex_from_loc.y as f32),
|
||||||
|
),
|
||||||
|
Uniform::new(
|
||||||
|
"tex_from_size",
|
||||||
|
(tex_from_size.x as f32, tex_from_size.y as f32),
|
||||||
|
),
|
||||||
|
Uniform::new("tex_to_loc", (tex_to_loc.x as f32, tex_to_loc.y as f32)),
|
||||||
|
Uniform::new("tex_to_size", (tex_to_size.x as f32, tex_to_size.y as f32)),
|
||||||
|
Uniform::new("amount", amount),
|
||||||
|
],
|
||||||
|
Kind::Unspecified,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for CrossfadeRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
self.0.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.0.current_commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.0.geometry(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
self.0.transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
self.0.src()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
commit: Option<CommitCounter>,
|
||||||
|
) -> DamageSet<i32, Physical> {
|
||||||
|
self.0.damage_since(scale, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
self.0.opaque_regions(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
self.0.alpha()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
self.0.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for CrossfadeRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||||
|
self.0.underlying_storage(renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render> RenderElement<TtyRenderer<'render>> for CrossfadeRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), TtyRendererError<'render>> {
|
||||||
|
let gles_frame = frame.as_gles_frame();
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
|
||||||
|
self.0.underlying_storage(renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
use glam::Vec2;
|
||||||
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
|
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||||
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||||
|
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
||||||
|
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::primary_gpu_pixel_shader::PrimaryGpuPixelShaderRenderElement;
|
||||||
|
use super::renderer::NiriRenderer;
|
||||||
|
use super::shaders::Shaders;
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
/// Renders a sub- or super-rect of an angled linear gradient like CSS linear-gradient(angle, a, b).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GradientRenderElement(PrimaryGpuPixelShaderRenderElement);
|
||||||
|
|
||||||
|
impl GradientRenderElement {
|
||||||
|
pub fn new(
|
||||||
|
renderer: &mut impl NiriRenderer,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
area: Rectangle<i32, Logical>,
|
||||||
|
gradient_area: Rectangle<i32, Logical>,
|
||||||
|
color_from: [f32; 4],
|
||||||
|
color_to: [f32; 4],
|
||||||
|
angle: f32,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let shader = Shaders::get(renderer).gradient_border.clone()?;
|
||||||
|
let grad_offset = (area.loc - gradient_area.loc).to_f64().to_physical(scale);
|
||||||
|
|
||||||
|
let grad_dir = Vec2::from_angle(angle);
|
||||||
|
|
||||||
|
let grad_area_size = gradient_area.size.to_f64().to_physical(scale);
|
||||||
|
let (w, h) = (grad_area_size.w as f32, grad_area_size.h as f32);
|
||||||
|
|
||||||
|
let mut grad_area_diag = Vec2::new(w, h);
|
||||||
|
if (grad_dir.x < 0. && 0. <= grad_dir.y) || (0. <= grad_dir.x && grad_dir.y < 0.) {
|
||||||
|
grad_area_diag.x = -w;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
|
||||||
|
if grad_dir.y <= 0. {
|
||||||
|
grad_vec = -grad_vec;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem = PixelShaderElement::new(
|
||||||
|
shader,
|
||||||
|
area,
|
||||||
|
None,
|
||||||
|
1.,
|
||||||
|
vec![
|
||||||
|
Uniform::new("color_from", color_from),
|
||||||
|
Uniform::new("color_to", color_to),
|
||||||
|
Uniform::new("grad_offset", (grad_offset.x as f32, grad_offset.y as f32)),
|
||||||
|
Uniform::new("grad_width", w),
|
||||||
|
Uniform::new("grad_vec", grad_vec.to_array()),
|
||||||
|
],
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
Some(Self(PrimaryGpuPixelShaderRenderElement(elem)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for GradientRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
self.0.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.0.current_commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.0.geometry(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
self.0.transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
self.0.src()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
commit: Option<CommitCounter>,
|
||||||
|
) -> DamageSet<i32, Physical> {
|
||||||
|
self.0.damage_since(scale, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
self.0.opaque_regions(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
self.0.alpha()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
self.0.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for GradientRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> {
|
||||||
|
self.0.underlying_storage(renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render> RenderElement<TtyRenderer<'render>> for GradientRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), TtyRendererError<'render>> {
|
||||||
|
RenderElement::<TtyRenderer<'_>>::draw(&self.0, frame, src, dst, damage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> {
|
||||||
|
self.0.underlying_storage(renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use anyhow::{ensure, Context};
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||||
|
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::sync::SyncPoint;
|
||||||
|
use smithay::backend::renderer::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||||
|
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||||
|
use smithay::wayland::shm;
|
||||||
|
|
||||||
|
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||||
|
|
||||||
|
pub mod crossfade;
|
||||||
|
pub mod gradient;
|
||||||
|
pub mod offscreen;
|
||||||
|
pub mod primary_gpu_pixel_shader;
|
||||||
|
pub mod primary_gpu_pixel_shader_with_textures;
|
||||||
|
pub mod primary_gpu_texture;
|
||||||
|
pub mod render_elements;
|
||||||
|
pub mod renderer;
|
||||||
|
pub mod resources;
|
||||||
|
pub mod shaders;
|
||||||
|
pub mod snapshot;
|
||||||
|
pub mod surface;
|
||||||
|
|
||||||
|
/// What we're rendering for.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum RenderTarget {
|
||||||
|
/// Rendering to display on screen.
|
||||||
|
Output,
|
||||||
|
/// Rendering for a screencast.
|
||||||
|
Screencast,
|
||||||
|
/// Rendering for any other screen capture.
|
||||||
|
ScreenCapture,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Buffer with location, src and dst.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BakedBuffer<B> {
|
||||||
|
pub buffer: B,
|
||||||
|
pub location: Point<i32, Logical>,
|
||||||
|
pub src: Option<Rectangle<f64, Logical>>,
|
||||||
|
pub dst: Option<Size<i32, Logical>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToRenderElement {
|
||||||
|
type RenderElement;
|
||||||
|
|
||||||
|
fn to_render_element(
|
||||||
|
&self,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
alpha: f32,
|
||||||
|
kind: Kind,
|
||||||
|
) -> Self::RenderElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToRenderElement for BakedBuffer<TextureBuffer<GlesTexture>> {
|
||||||
|
type RenderElement = PrimaryGpuTextureRenderElement;
|
||||||
|
|
||||||
|
fn to_render_element(
|
||||||
|
&self,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
alpha: f32,
|
||||||
|
kind: Kind,
|
||||||
|
) -> Self::RenderElement {
|
||||||
|
let elem = TextureRenderElement::from_texture_buffer(
|
||||||
|
(location + self.location).to_physical_precise_round(scale),
|
||||||
|
&self.buffer,
|
||||||
|
Some(alpha),
|
||||||
|
self.src,
|
||||||
|
self.dst,
|
||||||
|
kind,
|
||||||
|
);
|
||||||
|
PrimaryGpuTextureRenderElement(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToRenderElement for BakedBuffer<SolidColorBuffer> {
|
||||||
|
type RenderElement = SolidColorRenderElement;
|
||||||
|
|
||||||
|
fn to_render_element(
|
||||||
|
&self,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
alpha: f32,
|
||||||
|
kind: Kind,
|
||||||
|
) -> Self::RenderElement {
|
||||||
|
SolidColorRenderElement::from_buffer(
|
||||||
|
&self.buffer,
|
||||||
|
(location + self.location)
|
||||||
|
.to_physical_precise_round(scale)
|
||||||
|
.to_i32_round(),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
kind,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_to_encompassing_texture(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
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 elements = elements.iter().rev().map(|ele| {
|
||||||
|
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
|
||||||
|
});
|
||||||
|
|
||||||
|
let (texture, sync_point) =
|
||||||
|
render_to_texture(renderer, geo.size, scale, transform, fourcc, elements)?;
|
||||||
|
|
||||||
|
Ok((texture, sync_point, geo))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_to_texture(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
fourcc: Fourcc,
|
||||||
|
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||||
|
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||||
|
|
||||||
|
let texture: GlesTexture = renderer
|
||||||
|
.create_buffer(fourcc, buffer_size)
|
||||||
|
.context("error creating texture")?;
|
||||||
|
|
||||||
|
renderer
|
||||||
|
.bind(texture.clone())
|
||||||
|
.context("error binding texture")?;
|
||||||
|
|
||||||
|
let sync_point = render_elements(renderer, size, scale, transform, elements)?;
|
||||||
|
Ok((texture, sync_point))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_and_download(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
fourcc: Fourcc,
|
||||||
|
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||||
|
) -> anyhow::Result<GlesMapping> {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
let (_, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
|
||||||
|
|
||||||
|
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
|
||||||
|
.context("error copying framebuffer")?;
|
||||||
|
Ok(mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_to_vec(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
fourcc: Fourcc,
|
||||||
|
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
let mapping = render_and_download(renderer, size, scale, transform, fourcc, elements)
|
||||||
|
.context("error rendering")?;
|
||||||
|
let copy = renderer
|
||||||
|
.map_texture(&mapping)
|
||||||
|
.context("error mapping texture")?;
|
||||||
|
Ok(copy.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "xdp-gnome-screencast")]
|
||||||
|
pub fn render_to_dmabuf(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||||
|
) -> anyhow::Result<SyncPoint> {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
renderer.bind(dmabuf).context("error binding texture")?;
|
||||||
|
render_elements(renderer, size, scale, transform, elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_to_shm(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
buffer: &WlBuffer,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
let buffer_size = buffer_dimensions(buffer).context("error getting buffer dimensions")?;
|
||||||
|
let size = buffer_size.to_logical(1, Transform::Normal).to_physical(1);
|
||||||
|
|
||||||
|
let mapping =
|
||||||
|
render_and_download(renderer, size, scale, transform, Fourcc::Argb8888, elements)?;
|
||||||
|
let bytes = renderer
|
||||||
|
.map_texture(&mapping)
|
||||||
|
.context("error mapping texture")?;
|
||||||
|
|
||||||
|
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
|
||||||
|
ensure!(
|
||||||
|
// The buffer prefers pixels in little endian ...
|
||||||
|
buffer_data.format == wl_shm::Format::Argb8888
|
||||||
|
&& buffer_data.stride == size.w * 4
|
||||||
|
&& buffer_data.height == size.h
|
||||||
|
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
|
||||||
|
"invalid buffer format or size"
|
||||||
|
);
|
||||||
|
|
||||||
|
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let _span = tracy_client::span!("copy_nonoverlapping");
|
||||||
|
ptr::copy_nonoverlapping(bytes.as_ptr(), shm_buffer.cast(), shm_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.context("expected shm buffer, but didn't get one")?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_elements(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
transform: Transform,
|
||||||
|
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||||
|
) -> anyhow::Result<SyncPoint> {
|
||||||
|
let transform = transform.invert();
|
||||||
|
let output_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size));
|
||||||
|
|
||||||
|
let mut frame = renderer
|
||||||
|
.render(size, transform)
|
||||||
|
.context("error starting frame")?;
|
||||||
|
|
||||||
|
frame
|
||||||
|
.clear([0., 0., 0., 0.], &[output_rect])
|
||||||
|
.context("error clearing")?;
|
||||||
|
|
||||||
|
for element in elements {
|
||||||
|
let src = element.src();
|
||||||
|
let dst = element.geometry(scale);
|
||||||
|
|
||||||
|
if let Some(mut damage) = output_rect.intersection(dst) {
|
||||||
|
damage.loc -= dst.loc;
|
||||||
|
element
|
||||||
|
.draw(&mut frame, src, dst, &[damage])
|
||||||
|
.context("error drawing element")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.finish().context("error finishing frame")
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||||
|
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};
|
||||||
|
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||||
|
use super::render_to_texture;
|
||||||
|
use super::renderer::AsGlesFrame;
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
/// Renders elements into an off-screen buffer.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OffscreenRenderElement {
|
||||||
|
// The texture, if rendering succeeded.
|
||||||
|
texture: Option<PrimaryGpuTextureRenderElement>,
|
||||||
|
// The fallback buffer in case the rendering fails.
|
||||||
|
fallback: SolidColorRenderElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
let elements = elements.iter().rev().map(|ele| {
|
||||||
|
RelocateRenderElement::from_element(ele, (-geo.loc.x, -geo.loc.y), Relocate::Relative)
|
||||||
|
});
|
||||||
|
|
||||||
|
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, Transform::Normal, None);
|
||||||
|
let element = TextureRenderElement::from_texture_buffer(
|
||||||
|
geo.loc.to_f64(),
|
||||||
|
&buffer,
|
||||||
|
Some(result_alpha),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
texture: Some(PrimaryGpuTextureRenderElement(element)),
|
||||||
|
fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error off-screening elements: {err:?}");
|
||||||
|
Self {
|
||||||
|
texture: None,
|
||||||
|
fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for OffscreenRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.id()
|
||||||
|
} else {
|
||||||
|
self.fallback.id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.current_commit()
|
||||||
|
} else {
|
||||||
|
self.fallback.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.transform()
|
||||||
|
} else {
|
||||||
|
self.fallback.transform()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.src()
|
||||||
|
} else {
|
||||||
|
self.fallback.src()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.opaque_regions(scale)
|
||||||
|
} else {
|
||||||
|
self.fallback.opaque_regions(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.alpha()
|
||||||
|
} else {
|
||||||
|
self.fallback.alpha()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
if let Some(texture) = &self.texture {
|
||||||
|
texture.kind()
|
||||||
|
} else {
|
||||||
|
self.fallback.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for OffscreenRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[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)?;
|
||||||
|
} else {
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[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)?;
|
||||||
|
} else {
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.fallback, gles_frame, src, dst, damage)?;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
|
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||||
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer};
|
||||||
|
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
||||||
|
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::renderer::AsGlesFrame;
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
/// Wrapper for a pixel shader from the primary GPU for rendering with the primary GPU.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PrimaryGpuPixelShaderRenderElement(pub PixelShaderElement);
|
||||||
|
|
||||||
|
impl Element for PrimaryGpuPixelShaderRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
self.0.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.0.current_commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.0.geometry(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
self.0.transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
self.0.src()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
commit: Option<CommitCounter>,
|
||||||
|
) -> DamageSet<i32, Physical> {
|
||||||
|
self.0.damage_since(scale, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
self.0.opaque_regions(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
self.0.alpha()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
self.0.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
let gles_frame = frame.as_gles_frame();
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 PrimaryGpuPixelShaderRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), TtyRendererError<'render>> {
|
||||||
|
let gles_frame = frame.as_gles_frame();
|
||||||
|
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use glam::{Mat3, Vec2};
|
||||||
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
|
use smithay::backend::renderer::gles::{
|
||||||
|
ffi, link_program, Capability, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform,
|
||||||
|
UniformDesc, UniformName,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
|
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size};
|
||||||
|
|
||||||
|
use super::renderer::AsGlesFrame;
|
||||||
|
use super::resources::Resources;
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
/// Wrapper for a pixel shader from the primary GPU for rendering with the primary GPU.
|
||||||
|
///
|
||||||
|
/// The shader accepts textures as input.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PrimaryGpuPixelShaderWithTexturesRenderElement {
|
||||||
|
shader: PixelWithTexturesProgram,
|
||||||
|
textures: HashMap<String, GlesTexture>,
|
||||||
|
id: Id,
|
||||||
|
commit_counter: CommitCounter,
|
||||||
|
area: Rectangle<i32, Logical>,
|
||||||
|
size: Size<f64, Buffer>,
|
||||||
|
opaque_regions: Vec<Rectangle<i32, Logical>>,
|
||||||
|
alpha: f32,
|
||||||
|
additional_uniforms: Vec<Uniform<'static>>,
|
||||||
|
kind: Kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PixelWithTexturesProgram(Rc<PixelWithTexturesProgramInner>);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct PixelWithTexturesProgramInner {
|
||||||
|
program: ffi::types::GLuint,
|
||||||
|
uniform_tex_matrix: ffi::types::GLint,
|
||||||
|
uniform_matrix: ffi::types::GLint,
|
||||||
|
uniform_size: ffi::types::GLint,
|
||||||
|
uniform_alpha: ffi::types::GLint,
|
||||||
|
attrib_vert: ffi::types::GLint,
|
||||||
|
attrib_vert_position: ffi::types::GLint,
|
||||||
|
additional_uniforms: HashMap<String, UniformDesc>,
|
||||||
|
texture_uniforms: HashMap<String, ffi::types::GLint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn compile_program(
|
||||||
|
gl: &ffi::Gles2,
|
||||||
|
src: &str,
|
||||||
|
additional_uniforms: &[UniformName<'_>],
|
||||||
|
texture_uniforms: &[&str],
|
||||||
|
// destruction_callback_sender: Sender<CleanupResource>,
|
||||||
|
) -> Result<PixelWithTexturesProgram, GlesError> {
|
||||||
|
let shader = src;
|
||||||
|
|
||||||
|
let program = unsafe { link_program(gl, include_str!("shaders/texture.vert"), shader)? };
|
||||||
|
|
||||||
|
let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated");
|
||||||
|
let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated");
|
||||||
|
let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated");
|
||||||
|
let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated");
|
||||||
|
let size = CStr::from_bytes_with_nul(b"size\0").expect("NULL terminated");
|
||||||
|
let alpha = CStr::from_bytes_with_nul(b"alpha\0").expect("NULL terminated");
|
||||||
|
|
||||||
|
Ok(PixelWithTexturesProgram(Rc::new(
|
||||||
|
PixelWithTexturesProgramInner {
|
||||||
|
program,
|
||||||
|
uniform_matrix: gl
|
||||||
|
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
|
||||||
|
uniform_tex_matrix: gl
|
||||||
|
.GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar),
|
||||||
|
uniform_size: gl
|
||||||
|
.GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar),
|
||||||
|
uniform_alpha: gl
|
||||||
|
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
|
||||||
|
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
|
||||||
|
attrib_vert_position: gl
|
||||||
|
.GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar),
|
||||||
|
additional_uniforms: additional_uniforms
|
||||||
|
.iter()
|
||||||
|
.map(|uniform| {
|
||||||
|
let name =
|
||||||
|
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
|
||||||
|
let location =
|
||||||
|
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
|
||||||
|
(
|
||||||
|
uniform.name.clone().into_owned(),
|
||||||
|
UniformDesc {
|
||||||
|
location,
|
||||||
|
type_: uniform.type_,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
texture_uniforms: texture_uniforms
|
||||||
|
.iter()
|
||||||
|
.map(|name_| {
|
||||||
|
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
|
||||||
|
let location =
|
||||||
|
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
|
||||||
|
(name_.to_string(), location)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PixelWithTexturesProgram {
|
||||||
|
pub fn compile(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
src: &str,
|
||||||
|
additional_uniforms: &[UniformName<'_>],
|
||||||
|
texture_uniforms: &[&str],
|
||||||
|
) -> Result<Self, GlesError> {
|
||||||
|
renderer.with_context(move |gl| unsafe {
|
||||||
|
compile_program(gl, src, additional_uniforms, texture_uniforms)
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryGpuPixelShaderWithTexturesRenderElement {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
shader: PixelWithTexturesProgram,
|
||||||
|
textures: HashMap<String, GlesTexture>,
|
||||||
|
area: Rectangle<i32, Logical>,
|
||||||
|
size: Size<f64, Buffer>,
|
||||||
|
opaque_regions: Option<Vec<Rectangle<i32, Logical>>>,
|
||||||
|
alpha: f32,
|
||||||
|
additional_uniforms: Vec<Uniform<'_>>,
|
||||||
|
kind: Kind,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
shader,
|
||||||
|
textures,
|
||||||
|
id: Id::new(),
|
||||||
|
commit_counter: CommitCounter::default(),
|
||||||
|
area,
|
||||||
|
size,
|
||||||
|
opaque_regions: opaque_regions.unwrap_or_default(),
|
||||||
|
alpha,
|
||||||
|
additional_uniforms: additional_uniforms
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| u.into_owned())
|
||||||
|
.collect(),
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for PrimaryGpuPixelShaderWithTexturesRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.commit_counter
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
Rectangle::from_loc_and_size((0., 0.), self.size.to_f64())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.area.to_physical_precise_round(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
||||||
|
self.opaque_regions
|
||||||
|
.iter()
|
||||||
|
.map(|region| region.to_physical_precise_round(scale))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> Kind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for PrimaryGpuPixelShaderWithTexturesRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dest: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
let frame = frame.as_gles_frame();
|
||||||
|
|
||||||
|
let Some(resources) = Resources::get(frame) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let mut resources = resources.borrow_mut();
|
||||||
|
|
||||||
|
let supports_instancing = frame.capabilities().contains(&Capability::Instancing);
|
||||||
|
|
||||||
|
// prepare the vertices
|
||||||
|
resources.vertices.clear();
|
||||||
|
if supports_instancing {
|
||||||
|
resources.vertices.extend(damage.iter().flat_map(|rect| {
|
||||||
|
let dest_size = dest.size;
|
||||||
|
|
||||||
|
let rect_constrained_loc = rect
|
||||||
|
.loc
|
||||||
|
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
|
||||||
|
let rect_clamped_size = rect.size.clamp(
|
||||||
|
(0, 0),
|
||||||
|
(dest_size.to_point() - rect_constrained_loc).to_size(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
|
||||||
|
[
|
||||||
|
rect.loc.x as f32,
|
||||||
|
rect.loc.y as f32,
|
||||||
|
rect.size.w as f32,
|
||||||
|
rect.size.h as f32,
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
resources.vertices.extend(damage.iter().flat_map(|rect| {
|
||||||
|
let dest_size = dest.size;
|
||||||
|
|
||||||
|
let rect_constrained_loc = rect
|
||||||
|
.loc
|
||||||
|
.constrain(Rectangle::from_extemities((0, 0), dest_size.to_point()));
|
||||||
|
let rect_clamped_size = rect.size.clamp(
|
||||||
|
(0, 0),
|
||||||
|
(dest_size.to_point() - rect_constrained_loc).to_size(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size);
|
||||||
|
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
|
||||||
|
(0..6).flat_map(move |_| {
|
||||||
|
[
|
||||||
|
rect.loc.x as f32,
|
||||||
|
rect.loc.y as f32,
|
||||||
|
rect.size.w as f32,
|
||||||
|
rect.size.h as f32,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if resources.vertices.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// dest position and scale
|
||||||
|
let mut matrix = Mat3::from_translation(Vec2::new(dest.loc.x as f32, dest.loc.y as f32));
|
||||||
|
|
||||||
|
let scale = src.size.to_f64() / dest.size.to_f64();
|
||||||
|
let tex_matrix = Mat3::from_scale(Vec2::new(scale.x as f32, scale.y as f32));
|
||||||
|
let tex_matrix =
|
||||||
|
Mat3::from_translation(Vec2::new(src.loc.x as f32, src.loc.y as f32)) * tex_matrix;
|
||||||
|
let tex_matrix = Mat3::from_scale(Vec2::new(
|
||||||
|
(1.0f64 / self.size.w) as f32,
|
||||||
|
(1.0f64 / self.size.h) as f32,
|
||||||
|
)) * tex_matrix;
|
||||||
|
|
||||||
|
//apply output transformation
|
||||||
|
matrix = Mat3::from_cols_array(frame.projection()) * matrix;
|
||||||
|
|
||||||
|
let program = &self.shader.0;
|
||||||
|
|
||||||
|
// render
|
||||||
|
frame.with_context(move |gl| -> Result<(), GlesError> {
|
||||||
|
unsafe {
|
||||||
|
for (i, texture) in self.textures.values().enumerate() {
|
||||||
|
gl.ActiveTexture(ffi::TEXTURE0 + i as u32);
|
||||||
|
gl.BindTexture(ffi::TEXTURE_2D, texture.tex_id());
|
||||||
|
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
|
||||||
|
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
|
||||||
|
gl.TexParameteri(
|
||||||
|
ffi::TEXTURE_2D,
|
||||||
|
ffi::TEXTURE_WRAP_S,
|
||||||
|
ffi::CLAMP_TO_BORDER as i32,
|
||||||
|
);
|
||||||
|
gl.TexParameteri(
|
||||||
|
ffi::TEXTURE_2D,
|
||||||
|
ffi::TEXTURE_WRAP_T,
|
||||||
|
ffi::CLAMP_TO_BORDER as i32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.UseProgram(program.program);
|
||||||
|
|
||||||
|
for (i, name) in self.textures.keys().enumerate() {
|
||||||
|
gl.Uniform1i(program.texture_uniforms[name], i as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.UniformMatrix3fv(
|
||||||
|
program.uniform_matrix,
|
||||||
|
1,
|
||||||
|
ffi::FALSE,
|
||||||
|
matrix.as_ref().as_ptr(),
|
||||||
|
);
|
||||||
|
gl.UniformMatrix3fv(
|
||||||
|
program.uniform_tex_matrix,
|
||||||
|
1,
|
||||||
|
ffi::FALSE,
|
||||||
|
tex_matrix.as_ref().as_ptr(),
|
||||||
|
);
|
||||||
|
gl.Uniform2f(program.uniform_size, dest.size.w as f32, dest.size.h as f32);
|
||||||
|
gl.Uniform1f(program.uniform_alpha, self.alpha);
|
||||||
|
|
||||||
|
for uniform in &self.additional_uniforms {
|
||||||
|
let desc =
|
||||||
|
program
|
||||||
|
.additional_uniforms
|
||||||
|
.get(&*uniform.name)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
GlesError::UnknownUniform(uniform.name.clone().into_owned())
|
||||||
|
})?;
|
||||||
|
uniform.value.set(gl, desc)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.EnableVertexAttribArray(program.attrib_vert as u32);
|
||||||
|
gl.BindBuffer(ffi::ARRAY_BUFFER, resources.vbos[0]);
|
||||||
|
gl.VertexAttribPointer(
|
||||||
|
program.attrib_vert as u32,
|
||||||
|
2,
|
||||||
|
ffi::FLOAT,
|
||||||
|
ffi::FALSE,
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// vert_position
|
||||||
|
gl.EnableVertexAttribArray(program.attrib_vert_position as u32);
|
||||||
|
gl.BindBuffer(ffi::ARRAY_BUFFER, resources.vbos[1]);
|
||||||
|
gl.BufferData(
|
||||||
|
ffi::ARRAY_BUFFER,
|
||||||
|
(std::mem::size_of::<ffi::types::GLfloat>() * resources.vertices.len())
|
||||||
|
as isize,
|
||||||
|
resources.vertices.as_ptr() as *const _,
|
||||||
|
ffi::STREAM_DRAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.VertexAttribPointer(
|
||||||
|
program.attrib_vert_position as u32,
|
||||||
|
4,
|
||||||
|
ffi::FLOAT,
|
||||||
|
ffi::FALSE,
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let damage_len = damage.len() as i32;
|
||||||
|
if supports_instancing {
|
||||||
|
gl.VertexAttribDivisor(program.attrib_vert as u32, 0);
|
||||||
|
gl.VertexAttribDivisor(program.attrib_vert_position as u32, 1);
|
||||||
|
gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len);
|
||||||
|
} else {
|
||||||
|
// When we have more than 10 rectangles, draw them in batches of 10.
|
||||||
|
for i in 0..(damage_len - 1) / 10 {
|
||||||
|
gl.DrawArrays(ffi::TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
// Set damage pointer to the next 10 rectangles.
|
||||||
|
let offset =
|
||||||
|
(i + 1) as usize * 6 * 4 * std::mem::size_of::<ffi::types::GLfloat>();
|
||||||
|
gl.VertexAttribPointer(
|
||||||
|
program.attrib_vert_position as u32,
|
||||||
|
4,
|
||||||
|
ffi::FLOAT,
|
||||||
|
ffi::FALSE,
|
||||||
|
0,
|
||||||
|
offset as *const _,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the up to 10 remaining rectangles.
|
||||||
|
let count = ((damage_len - 1) % 10 + 1) * 6;
|
||||||
|
gl.DrawArrays(ffi::TRIANGLES, 0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
|
||||||
|
gl.BindTexture(ffi::TEXTURE_2D, 0);
|
||||||
|
gl.ActiveTexture(ffi::TEXTURE0);
|
||||||
|
gl.BindTexture(ffi::TEXTURE_2D, 0);
|
||||||
|
gl.DisableVertexAttribArray(program.attrib_vert as u32);
|
||||||
|
gl.DisableVertexAttribArray(program.attrib_vert_position as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})??;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 PrimaryGpuPixelShaderWithTexturesRenderElement
|
||||||
|
{
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut TtyFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), TtyRendererError<'render>> {
|
||||||
|
let frame = frame.as_gles_frame();
|
||||||
|
|
||||||
|
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,81 +1,12 @@
|
|||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
|
||||||
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
use smithay::backend::renderer::element::texture::TextureRenderElement;
|
||||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
||||||
use smithay::backend::renderer::utils::CommitCounter;
|
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
||||||
use smithay::backend::renderer::{
|
|
||||||
Bind, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, Texture,
|
|
||||||
};
|
|
||||||
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::renderer::AsGlesFrame;
|
||||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
/// Trait with our main renderer requirements to save on the typing.
|
|
||||||
pub trait NiriRenderer:
|
|
||||||
ImportAll
|
|
||||||
+ ImportMem
|
|
||||||
+ ExportMem
|
|
||||||
+ Bind<Dmabuf>
|
|
||||||
+ Offscreen<GlesTexture>
|
|
||||||
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
|
||||||
+ AsGlesRenderer
|
|
||||||
{
|
|
||||||
// Associated types to work around the instability of associated type bounds.
|
|
||||||
type NiriTextureId: Texture + Clone + 'static;
|
|
||||||
type NiriError: std::error::Error
|
|
||||||
+ Send
|
|
||||||
+ Sync
|
|
||||||
+ From<<GlesRenderer as Renderer>::Error>
|
|
||||||
+ 'static;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R> NiriRenderer for R
|
|
||||||
where
|
|
||||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
|
||||||
R::TextureId: Texture + Clone + 'static,
|
|
||||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
|
||||||
{
|
|
||||||
type NiriTextureId = R::TextureId;
|
|
||||||
type NiriError = R::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for getting the underlying `GlesRenderer`.
|
|
||||||
pub trait AsGlesRenderer {
|
|
||||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsGlesRenderer for GlesRenderer {
|
|
||||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'render, 'alloc> AsGlesRenderer for TtyRenderer<'render, 'alloc> {
|
|
||||||
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
|
||||||
self.as_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for getting the underlying `GlesFrame`.
|
|
||||||
pub trait AsGlesFrame<'frame>
|
|
||||||
where
|
|
||||||
Self: 'frame,
|
|
||||||
{
|
|
||||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
|
||||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'render, 'alloc, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'alloc, 'frame> {
|
|
||||||
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
|
||||||
self.as_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
|
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
|
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
|
||||||
@@ -105,7 +36,7 @@ impl Element for PrimaryGpuTextureRenderElement {
|
|||||||
&self,
|
&self,
|
||||||
scale: Scale<f64>,
|
scale: Scale<f64>,
|
||||||
commit: Option<CommitCounter>,
|
commit: Option<CommitCounter>,
|
||||||
) -> Vec<Rectangle<i32, Physical>> {
|
) -> DamageSet<i32, Physical> {
|
||||||
self.0.damage_since(scale, commit)
|
self.0.damage_since(scale, commit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,16 +73,14 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderElement {
|
||||||
for PrimaryGpuTextureRenderElement
|
|
||||||
{
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
frame: &mut TtyFrame<'_, '_, '_>,
|
frame: &mut TtyFrame<'_, '_>,
|
||||||
src: Rectangle<f64, Buffer>,
|
src: Rectangle<f64, Buffer>,
|
||||||
dst: Rectangle<i32, Physical>,
|
dst: Rectangle<i32, Physical>,
|
||||||
damage: &[Rectangle<i32, Physical>],
|
damage: &[Rectangle<i32, Physical>],
|
||||||
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
) -> Result<(), TtyRendererError<'render>> {
|
||||||
let gles_frame = frame.as_gles_frame();
|
let gles_frame = frame.as_gles_frame();
|
||||||
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -159,7 +88,7 @@ impl<'render, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>>
|
|||||||
|
|
||||||
fn underlying_storage(
|
fn underlying_storage(
|
||||||
&self,
|
&self,
|
||||||
_renderer: &mut TtyRenderer<'render, 'alloc>,
|
_renderer: &mut TtyRenderer<'render>,
|
||||||
) -> Option<UnderlyingStorage> {
|
) -> Option<UnderlyingStorage> {
|
||||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
||||||
// the target GPU into account.
|
// the target GPU into account.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// We need to implement RenderElement manually due to AsGlesFrame requirement.
|
||||||
|
// This macro does it for us.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! niri_render_elements {
|
||||||
|
// The two callable variants: with <R> and without <R>. They include From impls because nested
|
||||||
|
// repetitions ($type and $variant with + and $R with ?) don't work properly.
|
||||||
|
($name:ident<R> => { $($variant:ident = $type:ty),+ $(,)? }) => {
|
||||||
|
$crate::niri_render_elements!(@impl $name () ($name<R>) => { $($variant = $type),+ });
|
||||||
|
|
||||||
|
$(impl<R: $crate::render_helpers::renderer::NiriRenderer> From<$type> for $name<R> {
|
||||||
|
fn from(x: $type) -> Self {
|
||||||
|
Self::$variant(x)
|
||||||
|
}
|
||||||
|
})+
|
||||||
|
};
|
||||||
|
|
||||||
|
($name:ident => { $($variant:ident = $type:ty),+ $(,)? }) => {
|
||||||
|
$crate::niri_render_elements!(@impl $name ($name) () => { $($variant = $type),+ });
|
||||||
|
|
||||||
|
$(impl From<$type> for $name {
|
||||||
|
fn from(x: $type) -> Self {
|
||||||
|
Self::$variant(x)
|
||||||
|
}
|
||||||
|
})+
|
||||||
|
};
|
||||||
|
|
||||||
|
// The internal variant that generates most of the code. $name_no_R and $name_R are necessary
|
||||||
|
// for the impl RenderElement<SomeRenderer> for $name<SomeRenderer>: since $R does not appear
|
||||||
|
// in this line, we cannot condition based on $R like elsewhere, so we condition on duplicate
|
||||||
|
// names instead. Like this: $($name_R<SomeRenderer>)? $($name_no_R)? so only one is chosen.
|
||||||
|
(@impl $name:ident ($($name_no_R:ident)?) ($($name_R:ident<$R:ident>)?) => { $($variant:ident = $type:ty),+ }) => {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum $name$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? {
|
||||||
|
$($variant($type)),+
|
||||||
|
}
|
||||||
|
|
||||||
|
impl$(<$R: $crate::render_helpers::renderer::NiriRenderer>)? smithay::backend::renderer::element::Element for $name$(<$R>)? {
|
||||||
|
fn id(&self) -> &smithay::backend::renderer::element::Id {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.id()),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.current_commit()),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: smithay::utils::Scale<f64>) -> smithay::utils::Rectangle<i32, smithay::utils::Physical> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.geometry(scale)),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> smithay::utils::Transform {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.transform()),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> smithay::utils::Rectangle<f64, smithay::utils::Buffer> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.src()),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: smithay::utils::Scale<f64>,
|
||||||
|
commit: Option<smithay::backend::renderer::utils::CommitCounter>,
|
||||||
|
) -> smithay::backend::renderer::utils::DamageSet<i32, smithay::utils::Physical> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.damage_since(scale, commit)),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: smithay::utils::Scale<f64>) -> Vec<smithay::utils::Rectangle<i32, smithay::utils::Physical>> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.opaque_regions(scale)),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.alpha()),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> smithay::backend::renderer::element::Kind {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.kind()),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
|
||||||
|
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
|
||||||
|
{
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
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>],
|
||||||
|
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => {
|
||||||
|
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage)
|
||||||
|
})+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, renderer: &mut smithay::backend::renderer::gles::GlesRenderer) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
|
||||||
|
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
|
||||||
|
{
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
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>],
|
||||||
|
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => {
|
||||||
|
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage)
|
||||||
|
})+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(
|
||||||
|
&self,
|
||||||
|
renderer: &mut $crate::backend::tty::TtyRenderer<'render>,
|
||||||
|
) -> Option<smithay::backend::renderer::element::UnderlyingStorage> {
|
||||||
|
match self {
|
||||||
|
$($name::$variant(elem) => elem.underlying_storage(renderer)),+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer};
|
||||||
|
|
||||||
|
/// Trait with our main renderer requirements to save on the typing.
|
||||||
|
pub trait NiriRenderer:
|
||||||
|
ImportAll
|
||||||
|
+ ImportMem
|
||||||
|
+ ExportMem
|
||||||
|
+ Bind<Dmabuf>
|
||||||
|
+ Offscreen<GlesTexture>
|
||||||
|
+ Renderer<TextureId = Self::NiriTextureId, Error = Self::NiriError>
|
||||||
|
+ AsGlesRenderer
|
||||||
|
{
|
||||||
|
// Associated types to work around the instability of associated type bounds.
|
||||||
|
type NiriTextureId: Texture + Clone + 'static;
|
||||||
|
type NiriError: std::error::Error
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ From<<GlesRenderer as Renderer>::Error>
|
||||||
|
+ 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> NiriRenderer for R
|
||||||
|
where
|
||||||
|
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||||
|
R::TextureId: Texture + Clone + 'static,
|
||||||
|
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||||
|
{
|
||||||
|
type NiriTextureId = R::TextureId;
|
||||||
|
type NiriError = R::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for getting the underlying `GlesRenderer`.
|
||||||
|
pub trait AsGlesRenderer {
|
||||||
|
fn as_gles_renderer(&mut self) -> &mut GlesRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGlesRenderer for GlesRenderer {
|
||||||
|
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render> AsGlesRenderer for TtyRenderer<'render> {
|
||||||
|
fn as_gles_renderer(&mut self) -> &mut GlesRenderer {
|
||||||
|
self.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for getting the underlying `GlesFrame`.
|
||||||
|
pub trait AsGlesFrame<'frame>
|
||||||
|
where
|
||||||
|
Self: 'frame,
|
||||||
|
{
|
||||||
|
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'frame> AsGlesFrame<'frame> for GlesFrame<'frame> {
|
||||||
|
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'render, 'frame> AsGlesFrame<'frame> for TtyFrame<'render, 'frame> {
|
||||||
|
fn as_gles_frame(&mut self) -> &mut GlesFrame<'frame> {
|
||||||
|
self.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use smithay::backend::renderer::gles::{ffi, Capability, GlesError, GlesFrame, GlesRenderer};
|
||||||
|
|
||||||
|
pub struct Resources {
|
||||||
|
pub vertices: Vec<f32>,
|
||||||
|
pub vbos: [ffi::types::GLuint; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
static INSTANCED_VERTS: [ffi::types::GLfloat; 8] = [
|
||||||
|
1.0, 0.0, // top right
|
||||||
|
0.0, 0.0, // top left
|
||||||
|
1.0, 1.0, // bottom right
|
||||||
|
0.0, 1.0, // bottom left
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Vertices for rendering individual triangles.
|
||||||
|
const MAX_RECTS_PER_DRAW: usize = 10;
|
||||||
|
const TRIANGLE_VERTS: [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] = triangle_verts();
|
||||||
|
const fn triangle_verts() -> [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] {
|
||||||
|
let mut verts = [0.; 12 * MAX_RECTS_PER_DRAW];
|
||||||
|
let mut i = 0;
|
||||||
|
loop {
|
||||||
|
// Top Left.
|
||||||
|
verts[i * 12] = 0.0;
|
||||||
|
verts[i * 12 + 1] = 0.0;
|
||||||
|
|
||||||
|
// Bottom left.
|
||||||
|
verts[i * 12 + 2] = 0.0;
|
||||||
|
verts[i * 12 + 3] = 1.0;
|
||||||
|
|
||||||
|
// Bottom right.
|
||||||
|
verts[i * 12 + 4] = 1.0;
|
||||||
|
verts[i * 12 + 5] = 1.0;
|
||||||
|
|
||||||
|
// Top left.
|
||||||
|
verts[i * 12 + 6] = 0.0;
|
||||||
|
verts[i * 12 + 7] = 0.0;
|
||||||
|
|
||||||
|
// Bottom right.
|
||||||
|
verts[i * 12 + 8] = 1.0;
|
||||||
|
verts[i * 12 + 9] = 1.0;
|
||||||
|
|
||||||
|
// Top right.
|
||||||
|
verts[i * 12 + 10] = 1.0;
|
||||||
|
verts[i * 12 + 11] = 0.0;
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
if i == MAX_RECTS_PER_DRAW {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verts
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resources {
|
||||||
|
fn create(renderer: &mut GlesRenderer) -> Result<Self, GlesError> {
|
||||||
|
let _span = tracy_client::span!("Resources::init");
|
||||||
|
|
||||||
|
let supports_instancing = renderer.capabilities().contains(&Capability::Instancing);
|
||||||
|
renderer.with_context(|gl| unsafe {
|
||||||
|
let vertices: &[ffi::types::GLfloat] = if supports_instancing {
|
||||||
|
&INSTANCED_VERTS
|
||||||
|
} else {
|
||||||
|
&TRIANGLE_VERTS
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut vbos = [0; 2];
|
||||||
|
gl.GenBuffers(vbos.len() as i32, vbos.as_mut_ptr());
|
||||||
|
gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]);
|
||||||
|
gl.BufferData(
|
||||||
|
ffi::ARRAY_BUFFER,
|
||||||
|
std::mem::size_of_val(vertices) as isize,
|
||||||
|
vertices.as_ptr() as *const _,
|
||||||
|
ffi::STATIC_DRAW,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
vertices: vec![],
|
||||||
|
vbos,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(frame: &mut GlesFrame) -> Option<Rc<RefCell<Self>>> {
|
||||||
|
let data = frame.egl_context().user_data();
|
||||||
|
data.get().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(renderer: &mut GlesRenderer) {
|
||||||
|
match Resources::create(renderer) {
|
||||||
|
Ok(resources) => {
|
||||||
|
let data = renderer.egl_context().user_data();
|
||||||
|
if !data.insert_if_missing(|| Rc::new(RefCell::new(resources))) {
|
||||||
|
error!("resources were already initialized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error creating resources for rendering: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#version 100
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform sampler2D tex_from;
|
||||||
|
uniform vec2 tex_from_loc;
|
||||||
|
uniform vec2 tex_from_size;
|
||||||
|
|
||||||
|
uniform sampler2D tex_to;
|
||||||
|
uniform vec2 tex_to_loc;
|
||||||
|
uniform vec2 tex_to_size;
|
||||||
|
|
||||||
|
uniform float alpha;
|
||||||
|
uniform float amount;
|
||||||
|
|
||||||
|
uniform vec2 size;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 coords_from = (v_coords - tex_from_loc) / tex_from_size;
|
||||||
|
vec2 coords_to = (v_coords - tex_to_loc) / tex_to_size;
|
||||||
|
|
||||||
|
vec4 color_from = texture2D(tex_from, coords_from);
|
||||||
|
vec4 color_to = texture2D(tex_to, coords_to);
|
||||||
|
|
||||||
|
vec4 color = mix(color_from, color_to, amount);
|
||||||
|
color = color * alpha;
|
||||||
|
|
||||||
|
gl_FragColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
precision mediump float;
|
||||||
|
uniform float alpha;
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
uniform float tint;
|
||||||
|
#endif
|
||||||
|
uniform vec2 size;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
uniform vec4 color_from;
|
||||||
|
uniform vec4 color_to;
|
||||||
|
uniform vec2 grad_offset;
|
||||||
|
uniform float grad_width;
|
||||||
|
uniform vec2 grad_vec;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 coords = v_coords * size + grad_offset;
|
||||||
|
|
||||||
|
if ((grad_vec.x < 0.0 && 0.0 <= grad_vec.y) || (0.0 <= grad_vec.x && grad_vec.y < 0.0))
|
||||||
|
coords.x -= grad_width;
|
||||||
|
|
||||||
|
float frac = dot(coords, grad_vec) / dot(grad_vec, grad_vec);
|
||||||
|
|
||||||
|
if (grad_vec.y < 0.0)
|
||||||
|
frac += 1.0;
|
||||||
|
|
||||||
|
frac = clamp(frac, 0.0, 1.0);
|
||||||
|
vec4 out_color = mix(color_from, color_to, frac);
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
if (tint == 1.0)
|
||||||
|
out_color = vec4(0.0, 0.3, 0.0, 0.2) + out_color * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
gl_FragColor = out_color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
use smithay::backend::renderer::gles::{GlesPixelProgram, GlesRenderer, UniformName, UniformType};
|
||||||
|
|
||||||
|
use super::primary_gpu_pixel_shader_with_textures::PixelWithTexturesProgram;
|
||||||
|
use super::renderer::NiriRenderer;
|
||||||
|
|
||||||
|
pub struct Shaders {
|
||||||
|
pub gradient_border: Option<GlesPixelProgram>,
|
||||||
|
pub crossfade: Option<PixelWithTexturesProgram>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shaders {
|
||||||
|
fn compile(renderer: &mut GlesRenderer) -> Self {
|
||||||
|
let _span = tracy_client::span!("Shaders::compile");
|
||||||
|
|
||||||
|
let gradient_border = renderer
|
||||||
|
.compile_custom_pixel_shader(
|
||||||
|
include_str!("gradient_border.frag"),
|
||||||
|
&[
|
||||||
|
UniformName::new("color_from", UniformType::_4f),
|
||||||
|
UniformName::new("color_to", UniformType::_4f),
|
||||||
|
UniformName::new("grad_offset", UniformType::_2f),
|
||||||
|
UniformName::new("grad_width", UniformType::_1f),
|
||||||
|
UniformName::new("grad_vec", UniformType::_2f),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!("error compiling gradient border shader: {err:?}");
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let crossfade = PixelWithTexturesProgram::compile(
|
||||||
|
renderer,
|
||||||
|
include_str!("crossfade.frag"),
|
||||||
|
&[
|
||||||
|
UniformName::new("tex_from_loc", UniformType::_2f),
|
||||||
|
UniformName::new("tex_from_size", UniformType::_2f),
|
||||||
|
UniformName::new("tex_to_loc", UniformType::_2f),
|
||||||
|
UniformName::new("tex_to_size", UniformType::_2f),
|
||||||
|
UniformName::new("amount", UniformType::_1f),
|
||||||
|
],
|
||||||
|
&["tex_from", "tex_to"],
|
||||||
|
)
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!("error compiling crossfade shader: {err:?}");
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
gradient_border,
|
||||||
|
crossfade,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(renderer: &mut impl NiriRenderer) -> &Self {
|
||||||
|
let renderer = renderer.as_gles_renderer();
|
||||||
|
let data = renderer.egl_context().user_data();
|
||||||
|
data.get()
|
||||||
|
.expect("shaders::init() must be called when creating the renderer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(renderer: &mut GlesRenderer) {
|
||||||
|
let shaders = Shaders::compile(renderer);
|
||||||
|
let data = renderer.egl_context().user_data();
|
||||||
|
if !data.insert_if_missing(|| shaders) {
|
||||||
|
error!("shaders were already compiled");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#version 100
|
||||||
|
|
||||||
|
uniform mat3 matrix;
|
||||||
|
uniform mat3 tex_matrix;
|
||||||
|
|
||||||
|
attribute vec2 vert;
|
||||||
|
attribute vec4 vert_position;
|
||||||
|
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
mat2 scale(vec2 scale_vec){
|
||||||
|
return mat2(
|
||||||
|
scale_vec.x, 0.0,
|
||||||
|
0.0, scale_vec.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 vert_transform_translation = vert_position.xy;
|
||||||
|
vec2 vert_transform_scale = vert_position.zw;
|
||||||
|
vec3 position = vec3(vert * scale(vert_transform_scale) + vert_transform_translation, 1.0);
|
||||||
|
v_coords = (tex_matrix * position).xy;
|
||||||
|
gl_Position = vec4(matrix * position, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
|
use niri_config::BlockOutFrom;
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||||
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
|
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||||
|
|
||||||
|
use super::{render_to_encompassing_texture, RenderTarget, ToRenderElement};
|
||||||
|
|
||||||
|
/// Snapshot of a render.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RenderSnapshot<C, B> {
|
||||||
|
/// Contents for a normal render.
|
||||||
|
///
|
||||||
|
/// Relative to the geometry.
|
||||||
|
pub contents: Vec<C>,
|
||||||
|
|
||||||
|
/// Blocked-out contents.
|
||||||
|
///
|
||||||
|
/// Relative to the geometry.
|
||||||
|
pub blocked_out_contents: Vec<B>,
|
||||||
|
|
||||||
|
/// Where the contents were blocked out from at the time of the snapshot.
|
||||||
|
pub block_out_from: Option<BlockOutFrom>,
|
||||||
|
|
||||||
|
/// Visual size of the element at the point of the snapshot.
|
||||||
|
pub size: Size<i32, Logical>,
|
||||||
|
|
||||||
|
/// Contents rendered into a texture (lazily).
|
||||||
|
pub texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
|
||||||
|
|
||||||
|
/// Blocked-out contents rendered into a texture (lazily).
|
||||||
|
pub blocked_out_texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C, B, EC, EB> RenderSnapshot<C, B>
|
||||||
|
where
|
||||||
|
C: ToRenderElement<RenderElement = EC>,
|
||||||
|
B: ToRenderElement<RenderElement = EB>,
|
||||||
|
EC: RenderElement<GlesRenderer>,
|
||||||
|
EB: RenderElement<GlesRenderer>,
|
||||||
|
{
|
||||||
|
pub fn texture(
|
||||||
|
&self,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> Option<&(GlesTexture, Rectangle<i32, Physical>)> {
|
||||||
|
let block_out = match self.block_out_from {
|
||||||
|
None => false,
|
||||||
|
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
|
||||||
|
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
|
||||||
|
};
|
||||||
|
|
||||||
|
if block_out {
|
||||||
|
self.blocked_out_texture.get_or_init(|| {
|
||||||
|
let _span = tracy_client::span!("RenderSnapshot::Texture");
|
||||||
|
|
||||||
|
let elements: Vec<_> = self
|
||||||
|
.blocked_out_contents
|
||||||
|
.iter()
|
||||||
|
.map(|baked| {
|
||||||
|
baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match render_to_encompassing_texture(
|
||||||
|
renderer,
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
&elements,
|
||||||
|
) {
|
||||||
|
Ok((texture, _sync_point, geo)) => Some((texture, geo)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error rendering blocked-out contents to texture: {err:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.texture.get_or_init(|| {
|
||||||
|
let _span = tracy_client::span!("RenderSnapshot::Texture");
|
||||||
|
|
||||||
|
let elements: Vec<_> = self
|
||||||
|
.contents
|
||||||
|
.iter()
|
||||||
|
.map(|baked| {
|
||||||
|
baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match render_to_encompassing_texture(
|
||||||
|
renderer,
|
||||||
|
scale,
|
||||||
|
Transform::Normal,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
&elements,
|
||||||
|
) {
|
||||||
|
Ok((texture, _sync_point, geo)) => Some((texture, geo)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error rendering contents to texture: {err:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use smithay::backend::renderer::element::texture::TextureBuffer;
|
||||||
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
|
use smithay::backend::renderer::utils::{import_surface, RendererSurfaceStateUserData};
|
||||||
|
use smithay::backend::renderer::Renderer as _;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
use smithay::utils::{Logical, Point};
|
||||||
|
use smithay::wayland::compositor::{with_surface_tree_downward, TraversalAction};
|
||||||
|
|
||||||
|
use super::BakedBuffer;
|
||||||
|
|
||||||
|
/// Renders elements from a surface tree as textures into `storage`.
|
||||||
|
pub fn render_snapshot_from_surface_tree(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
surface: &WlSurface,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
storage: &mut Vec<BakedBuffer<TextureBuffer<GlesTexture>>>,
|
||||||
|
) {
|
||||||
|
let _span = tracy_client::span!("render_snapshot_from_surface_tree");
|
||||||
|
|
||||||
|
with_surface_tree_downward(
|
||||||
|
surface,
|
||||||
|
location,
|
||||||
|
|_, states, location| {
|
||||||
|
let mut location = *location;
|
||||||
|
let data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||||
|
|
||||||
|
if let Some(data) = data {
|
||||||
|
let data = &*data.borrow();
|
||||||
|
|
||||||
|
if let Some(view) = data.view() {
|
||||||
|
location += view.offset;
|
||||||
|
TraversalAction::DoChildren(location)
|
||||||
|
} else {
|
||||||
|
TraversalAction::SkipChildren
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TraversalAction::SkipChildren
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_, states, location| {
|
||||||
|
let mut location = *location;
|
||||||
|
let data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||||
|
|
||||||
|
if let Some(data) = data {
|
||||||
|
if let Some(view) = data.borrow().view() {
|
||||||
|
location += view.offset;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = import_surface(renderer, states) {
|
||||||
|
warn!("failed to import surface: {err:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = data.borrow();
|
||||||
|
let view = data.view().unwrap();
|
||||||
|
let Some(texture) = data.texture::<GlesRenderer>(renderer.id()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer = TextureBuffer::from_texture(
|
||||||
|
renderer,
|
||||||
|
texture.clone(),
|
||||||
|
data.buffer_scale(),
|
||||||
|
data.buffer_transform(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let baked = BakedBuffer {
|
||||||
|
buffer,
|
||||||
|
location,
|
||||||
|
src: Some(view.src),
|
||||||
|
dst: Some(view.dst),
|
||||||
|
};
|
||||||
|
|
||||||
|
storage.push(baked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_, _, _| true,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct RubberBand {
|
||||||
|
pub stiffness: f64,
|
||||||
|
pub limit: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RubberBand {
|
||||||
|
pub fn band(&self, x: f64) -> f64 {
|
||||||
|
let c = self.stiffness;
|
||||||
|
let d = self.limit;
|
||||||
|
|
||||||
|
(1. - (1. / (x * c / d + 1.))) * d
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derivative(&self, x: f64) -> f64 {
|
||||||
|
let c = self.stiffness;
|
||||||
|
let d = self.limit;
|
||||||
|
|
||||||
|
c * d * d / (c * x + d).powi(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 {
|
||||||
|
let clamped = x.clamp(min, max);
|
||||||
|
let sign = if x < clamped { -1. } else { 1. };
|
||||||
|
let diff = (x - clamped).abs();
|
||||||
|
|
||||||
|
clamped + sign * self.band(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 {
|
||||||
|
if min <= x && x <= max {
|
||||||
|
return 1.;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clamped = x.clamp(min, max);
|
||||||
|
let diff = (x - clamped).abs();
|
||||||
|
self.derivative(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
pub struct ScrollTracker {
|
||||||
|
tick: f64,
|
||||||
|
last: f64,
|
||||||
|
acc: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollTracker {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new(tick: i8) -> Self {
|
||||||
|
Self {
|
||||||
|
tick: f64::from(tick),
|
||||||
|
last: 0.,
|
||||||
|
acc: 0.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accumulate(&mut self, amount: f64) -> i8 {
|
||||||
|
let changed_direction = (self.last > 0. && amount < 0.) || (self.last < 0. && amount > 0.);
|
||||||
|
if changed_direction {
|
||||||
|
self.acc = 0.
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last = amount;
|
||||||
|
self.acc += amount;
|
||||||
|
|
||||||
|
let mut ticks = 0;
|
||||||
|
if self.acc.abs() >= self.tick {
|
||||||
|
let clamped = self.acc.clamp(-127. * self.tick, 127. * self.tick);
|
||||||
|
ticks = (clamped as i16 / self.tick as i16) as i8;
|
||||||
|
self.acc %= self.tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
ticks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.last = 0.;
|
||||||
|
self.acc = 0.;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const HISTORY_LIMIT: Duration = Duration::from_millis(150);
|
||||||
|
const DECELERATION_TOUCHPAD: f64 = 0.997;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SwipeTracker {
|
||||||
|
history: VecDeque<Event>,
|
||||||
|
pos: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Event {
|
||||||
|
delta: f64,
|
||||||
|
timestamp: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwipeTracker {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
history: VecDeque::new(),
|
||||||
|
pos: 0.,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a new reading into the tracker.
|
||||||
|
pub fn push(&mut self, delta: f64, timestamp: Duration) {
|
||||||
|
// For the events that we care about, timestamps should always increase
|
||||||
|
// monotonically.
|
||||||
|
if let Some(last) = self.history.back() {
|
||||||
|
if timestamp < last.timestamp {
|
||||||
|
trace!(
|
||||||
|
"ignoring event with timestamp {timestamp:?} earlier than last {:?}",
|
||||||
|
last.timestamp
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history.push_back(Event { delta, timestamp });
|
||||||
|
self.pos += delta;
|
||||||
|
|
||||||
|
self.trim_history();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current gesture position.
|
||||||
|
pub fn pos(&self) -> f64 {
|
||||||
|
self.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the current gesture velocity.
|
||||||
|
pub fn velocity(&self) -> f64 {
|
||||||
|
let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else {
|
||||||
|
return 0.;
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_time = (last.timestamp - first.timestamp).as_secs_f64();
|
||||||
|
if total_time == 0. {
|
||||||
|
return 0.;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_delta = self.history.iter().map(|event| event.delta).sum::<f64>();
|
||||||
|
total_delta / total_time
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the gesture end position after decelerating to a halt.
|
||||||
|
pub fn projected_end_pos(&self) -> f64 {
|
||||||
|
let vel = self.velocity();
|
||||||
|
self.pos - vel / (1000. * DECELERATION_TOUCHPAD.ln())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_history(&mut self) {
|
||||||
|
let Some(&Event { timestamp, .. }) = self.history.back() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Some(first) = self.history.front() {
|
||||||
|
if timestamp <= first.timestamp + HISTORY_LIMIT {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.history.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use niri_config::Config;
|
||||||
use pangocairo::cairo::{self, ImageSurface};
|
use pangocairo::cairo::{self, ImageSurface};
|
||||||
use pangocairo::pango::FontDescription;
|
use pangocairo::pango::FontDescription;
|
||||||
use smithay::backend::renderer::element::memory::{
|
use smithay::backend::renderer::element::memory::{
|
||||||
@@ -14,7 +17,7 @@ use smithay::reexports::gbm::Format as Fourcc;
|
|||||||
use smithay::utils::Transform;
|
use smithay::utils::Transform;
|
||||||
|
|
||||||
use crate::animation::Animation;
|
use crate::animation::Animation;
|
||||||
use crate::render_helpers::NiriRenderer;
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
|
||||||
const TEXT: &str = "Failed to parse the config file. \
|
const TEXT: &str = "Failed to parse the config file. \
|
||||||
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
|
Please run <span face='monospace' bgcolor='#000000'>niri validate</span> \
|
||||||
@@ -26,6 +29,12 @@ const BORDER: i32 = 4;
|
|||||||
pub struct ConfigErrorNotification {
|
pub struct ConfigErrorNotification {
|
||||||
state: State,
|
state: State,
|
||||||
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
|
buffers: RefCell<HashMap<i32, Option<MemoryRenderBuffer>>>,
|
||||||
|
|
||||||
|
// If set, this is a "Created config at {path}" notification. If unset, this is a config error
|
||||||
|
// notification.
|
||||||
|
created_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
config: Rc<RefCell<Config>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
@@ -39,16 +48,37 @@ pub type ConfigErrorNotificationRenderElement<R> =
|
|||||||
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
RelocateRenderElement<MemoryRenderBufferRenderElement<R>>;
|
||||||
|
|
||||||
impl ConfigErrorNotification {
|
impl ConfigErrorNotification {
|
||||||
pub fn new() -> Self {
|
pub fn new(config: Rc<RefCell<Config>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: State::Hidden,
|
state: State::Hidden,
|
||||||
buffers: RefCell::new(HashMap::new()),
|
buffers: RefCell::new(HashMap::new()),
|
||||||
|
created_path: None,
|
||||||
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn animation(&self, from: f64, to: f64) -> Animation {
|
||||||
|
let c = self.config.borrow();
|
||||||
|
Animation::new(from, to, 0., c.animations.config_notification_open_close.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
|
||||||
|
if self.created_path != created_path {
|
||||||
|
self.created_path = created_path;
|
||||||
|
self.buffers.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = State::Showing(self.animation(0., 1.));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show(&mut self) {
|
pub fn show(&mut self) {
|
||||||
|
if self.created_path.is_some() {
|
||||||
|
self.created_path = None;
|
||||||
|
self.buffers.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Show from scratch even if already showing to bring attention.
|
// Show from scratch even if already showing to bring attention.
|
||||||
self.state = State::Showing(Animation::new(0., 1., Duration::from_millis(250)));
|
self.state = State::Showing(self.animation(0., 1.));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hide(&mut self) {
|
pub fn hide(&mut self) {
|
||||||
@@ -56,7 +86,7 @@ impl ConfigErrorNotification {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state = State::Hiding(Animation::new(1., 0., Duration::from_millis(250)));
|
self.state = State::Hiding(self.animation(1., 0.));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
|
pub fn advance_animations(&mut self, target_presentation_time: Duration) {
|
||||||
@@ -65,7 +95,15 @@ impl ConfigErrorNotification {
|
|||||||
State::Showing(anim) => {
|
State::Showing(anim) => {
|
||||||
anim.set_current_time(target_presentation_time);
|
anim.set_current_time(target_presentation_time);
|
||||||
if anim.is_done() {
|
if anim.is_done() {
|
||||||
self.state = State::Shown(target_presentation_time + Duration::from_secs(4));
|
let duration = if self.created_path.is_some() {
|
||||||
|
// Make this quite a bit longer because it comes with a monitor modeset
|
||||||
|
// (can take a while) and an important hotkeys popup diverting the
|
||||||
|
// attention.
|
||||||
|
Duration::from_secs(8)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(4)
|
||||||
|
};
|
||||||
|
self.state = State::Shown(target_presentation_time + duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Shown(deadline) => {
|
State::Shown(deadline) => {
|
||||||
@@ -75,7 +113,7 @@ impl ConfigErrorNotification {
|
|||||||
}
|
}
|
||||||
State::Hiding(anim) => {
|
State::Hiding(anim) => {
|
||||||
anim.set_current_time(target_presentation_time);
|
anim.set_current_time(target_presentation_time);
|
||||||
if anim.is_done() {
|
if anim.is_clamped_done() {
|
||||||
self.state = State::Hidden;
|
self.state = State::Hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,11 +134,12 @@ impl ConfigErrorNotification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let scale = output.current_scale().integer_scale();
|
let scale = output.current_scale().integer_scale();
|
||||||
|
let path = self.created_path.as_deref();
|
||||||
|
|
||||||
let mut buffers = self.buffers.borrow_mut();
|
let mut buffers = self.buffers.borrow_mut();
|
||||||
let buffer = buffers
|
let buffer = buffers
|
||||||
.entry(scale)
|
.entry(scale)
|
||||||
.or_insert_with_key(move |&scale| render(scale).ok());
|
.or_insert_with_key(move |&scale| render(scale, path).ok());
|
||||||
let buffer = buffer.as_ref()?;
|
let buffer = buffer.as_ref()?;
|
||||||
|
|
||||||
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
let elem = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
@@ -138,19 +177,30 @@ impl ConfigErrorNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
fn render(scale: i32, created_path: Option<&Path>) -> anyhow::Result<MemoryRenderBuffer> {
|
||||||
let _span = tracy_client::span!("config_error_notification::render");
|
let _span = tracy_client::span!("config_error_notification::render");
|
||||||
|
|
||||||
let padding = PADDING * scale;
|
let padding = PADDING * scale;
|
||||||
|
|
||||||
|
let mut text = String::from(TEXT);
|
||||||
|
let mut border_color = (1., 0.3, 0.3);
|
||||||
|
if let Some(path) = created_path {
|
||||||
|
text = format!(
|
||||||
|
"Created a default config file at \
|
||||||
|
<span face='monospace' bgcolor='#000000'>{:?}</span>",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
border_color = (0.5, 1., 0.5);
|
||||||
|
};
|
||||||
|
|
||||||
let mut font = FontDescription::from_string(FONT);
|
let mut font = FontDescription::from_string(FONT);
|
||||||
font.set_absolute_size((font.size() * scale).into());
|
font.set_absolute_size((font.size() * scale).into());
|
||||||
|
|
||||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||||
let cr = cairo::Context::new(&surface)?;
|
let cr = cairo::Context::new(&surface)?;
|
||||||
let layout = pangocairo::create_layout(&cr);
|
let layout = pangocairo::functions::create_layout(&cr);
|
||||||
layout.set_font_description(Some(&font));
|
layout.set_font_description(Some(&font));
|
||||||
layout.set_markup(TEXT);
|
layout.set_markup(&text);
|
||||||
|
|
||||||
let (mut width, mut height) = layout.pixel_size();
|
let (mut width, mut height) = layout.pixel_size();
|
||||||
width += padding * 2;
|
width += padding * 2;
|
||||||
@@ -166,25 +216,25 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
|||||||
cr.paint()?;
|
cr.paint()?;
|
||||||
|
|
||||||
cr.move_to(padding.into(), padding.into());
|
cr.move_to(padding.into(), padding.into());
|
||||||
let layout = pangocairo::create_layout(&cr);
|
let layout = pangocairo::functions::create_layout(&cr);
|
||||||
layout.set_font_description(Some(&font));
|
layout.set_font_description(Some(&font));
|
||||||
layout.set_markup(TEXT);
|
layout.set_markup(&text);
|
||||||
|
|
||||||
cr.set_source_rgb(1., 1., 1.);
|
cr.set_source_rgb(1., 1., 1.);
|
||||||
pangocairo::show_layout(&cr, &layout);
|
pangocairo::functions::show_layout(&cr, &layout);
|
||||||
|
|
||||||
cr.move_to(0., 0.);
|
cr.move_to(0., 0.);
|
||||||
cr.line_to(width.into(), 0.);
|
cr.line_to(width.into(), 0.);
|
||||||
cr.line_to(width.into(), height.into());
|
cr.line_to(width.into(), height.into());
|
||||||
cr.line_to(0., height.into());
|
cr.line_to(0., height.into());
|
||||||
cr.line_to(0., 0.);
|
cr.line_to(0., 0.);
|
||||||
cr.set_source_rgb(1., 0.3, 0.3);
|
cr.set_source_rgb(border_color.0, border_color.1, border_color.2);
|
||||||
cr.set_line_width((BORDER * scale).into());
|
cr.set_line_width((BORDER * scale).into());
|
||||||
cr.stroke()?;
|
cr.stroke()?;
|
||||||
drop(cr);
|
drop(cr);
|
||||||
|
|
||||||
let data = surface.take_data().unwrap();
|
let data = surface.take_data().unwrap();
|
||||||
let buffer = MemoryRenderBuffer::from_memory(
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
&data,
|
&data,
|
||||||
Fourcc::Argb8888,
|
Fourcc::Argb8888,
|
||||||
(width, height),
|
(width, height),
|
||||||
@@ -12,7 +12,7 @@ use smithay::output::Output;
|
|||||||
use smithay::reexports::gbm::Format as Fourcc;
|
use smithay::reexports::gbm::Format as Fourcc;
|
||||||
use smithay::utils::Transform;
|
use smithay::utils::Transform;
|
||||||
|
|
||||||
use crate::render_helpers::NiriRenderer;
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
|
||||||
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
|
const TEXT: &str = "Are you sure you want to exit niri?\n\n\
|
||||||
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
|
Press <span face='mono' bgcolor='#2C2C2C'> Enter </span> to confirm.";
|
||||||
@@ -111,7 +111,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
|||||||
|
|
||||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||||
let cr = cairo::Context::new(&surface)?;
|
let cr = cairo::Context::new(&surface)?;
|
||||||
let layout = pangocairo::create_layout(&cr);
|
let layout = pangocairo::functions::create_layout(&cr);
|
||||||
layout.set_font_description(Some(&font));
|
layout.set_font_description(Some(&font));
|
||||||
layout.set_alignment(Alignment::Center);
|
layout.set_alignment(Alignment::Center);
|
||||||
layout.set_markup(TEXT);
|
layout.set_markup(TEXT);
|
||||||
@@ -130,13 +130,13 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
|||||||
cr.paint()?;
|
cr.paint()?;
|
||||||
|
|
||||||
cr.move_to(padding.into(), padding.into());
|
cr.move_to(padding.into(), padding.into());
|
||||||
let layout = pangocairo::create_layout(&cr);
|
let layout = pangocairo::functions::create_layout(&cr);
|
||||||
layout.set_font_description(Some(&font));
|
layout.set_font_description(Some(&font));
|
||||||
layout.set_alignment(Alignment::Center);
|
layout.set_alignment(Alignment::Center);
|
||||||
layout.set_markup(TEXT);
|
layout.set_markup(TEXT);
|
||||||
|
|
||||||
cr.set_source_rgb(1., 1., 1.);
|
cr.set_source_rgb(1., 1., 1.);
|
||||||
pangocairo::show_layout(&cr, &layout);
|
pangocairo::functions::show_layout(&cr, &layout);
|
||||||
|
|
||||||
cr.move_to(0., 0.);
|
cr.move_to(0., 0.);
|
||||||
cr.line_to(width.into(), 0.);
|
cr.line_to(width.into(), 0.);
|
||||||
@@ -149,7 +149,7 @@ fn render(scale: i32) -> anyhow::Result<MemoryRenderBuffer> {
|
|||||||
drop(cr);
|
drop(cr);
|
||||||
|
|
||||||
let data = surface.take_data().unwrap();
|
let data = surface.take_data().unwrap();
|
||||||
let buffer = MemoryRenderBuffer::from_memory(
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
&data,
|
&data,
|
||||||
Fourcc::Argb8888,
|
Fourcc::Argb8888,
|
||||||
(width, height),
|
(width, height),
|
||||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
|||||||
use std::iter::zip;
|
use std::iter::zip;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use niri_config::{Action, Config, Key, Modifiers};
|
use niri_config::{Action, Config, Key, Modifiers, Trigger};
|
||||||
use pangocairo::cairo::{self, ImageSurface};
|
use pangocairo::cairo::{self, ImageSurface};
|
||||||
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
|
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
|
||||||
use smithay::backend::renderer::element::memory::{
|
use smithay::backend::renderer::element::memory::{
|
||||||
@@ -18,7 +18,7 @@ use smithay::reexports::gbm::Format as Fourcc;
|
|||||||
use smithay::utils::{Physical, Size, Transform};
|
use smithay::utils::{Physical, Size, Transform};
|
||||||
|
|
||||||
use crate::input::CompositorMod;
|
use crate::input::CompositorMod;
|
||||||
use crate::render_helpers::NiriRenderer;
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
|
||||||
const PADDING: i32 = 8;
|
const PADDING: i32 = 8;
|
||||||
const MARGIN: i32 = PADDING * 2;
|
const MARGIN: i32 = PADDING * 2;
|
||||||
@@ -155,13 +155,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
let binds = &config.binds.0;
|
let binds = &config.binds.0;
|
||||||
|
|
||||||
// Collect actions that we want to show.
|
// Collect actions that we want to show.
|
||||||
let mut actions = vec![
|
let mut actions = vec![&Action::ShowHotkeyOverlay];
|
||||||
&Action::ShowHotkeyOverlay,
|
|
||||||
&Action::Quit,
|
// Prefer Quit(false) if found, otherwise try Quit(true), and if there's neither, fall back to
|
||||||
&Action::CloseWindow,
|
// Quit(false).
|
||||||
];
|
if binds.iter().any(|bind| bind.action == Action::Quit(false)) {
|
||||||
|
actions.push(&Action::Quit(false));
|
||||||
|
} else if binds.iter().any(|bind| bind.action == Action::Quit(true)) {
|
||||||
|
actions.push(&Action::Quit(true));
|
||||||
|
} else {
|
||||||
|
actions.push(&Action::Quit(false));
|
||||||
|
}
|
||||||
|
|
||||||
actions.extend(&[
|
actions.extend(&[
|
||||||
|
&Action::CloseWindow,
|
||||||
&Action::FocusColumnLeft,
|
&Action::FocusColumnLeft,
|
||||||
&Action::FocusColumnRight,
|
&Action::FocusColumnRight,
|
||||||
&Action::MoveColumnLeft,
|
&Action::MoveColumnLeft,
|
||||||
@@ -173,12 +180,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
|
// Prefer move-column-to-workspace-down, but fall back to move-window-to-workspace-down.
|
||||||
if binds
|
if binds
|
||||||
.iter()
|
.iter()
|
||||||
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceDown))
|
.any(|bind| bind.action == Action::MoveColumnToWorkspaceDown)
|
||||||
{
|
{
|
||||||
actions.push(&Action::MoveColumnToWorkspaceDown);
|
actions.push(&Action::MoveColumnToWorkspaceDown);
|
||||||
} else if binds
|
} else if binds
|
||||||
.iter()
|
.iter()
|
||||||
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceDown))
|
.any(|bind| bind.action == Action::MoveWindowToWorkspaceDown)
|
||||||
{
|
{
|
||||||
actions.push(&Action::MoveWindowToWorkspaceDown);
|
actions.push(&Action::MoveWindowToWorkspaceDown);
|
||||||
} else {
|
} else {
|
||||||
@@ -188,12 +195,12 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
// Same for -up.
|
// Same for -up.
|
||||||
if binds
|
if binds
|
||||||
.iter()
|
.iter()
|
||||||
.any(|bind| bind.actions.first() == Some(&Action::MoveColumnToWorkspaceUp))
|
.any(|bind| bind.action == Action::MoveColumnToWorkspaceUp)
|
||||||
{
|
{
|
||||||
actions.push(&Action::MoveColumnToWorkspaceUp);
|
actions.push(&Action::MoveColumnToWorkspaceUp);
|
||||||
} else if binds
|
} else if binds
|
||||||
.iter()
|
.iter()
|
||||||
.any(|bind| bind.actions.first() == Some(&Action::MoveWindowToWorkspaceUp))
|
.any(|bind| bind.action == Action::MoveWindowToWorkspaceUp)
|
||||||
{
|
{
|
||||||
actions.push(&Action::MoveWindowToWorkspaceUp);
|
actions.push(&Action::MoveWindowToWorkspaceUp);
|
||||||
} else {
|
} else {
|
||||||
@@ -208,20 +215,28 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Screenshot is not as important, can omit if not bound.
|
// Screenshot is not as important, can omit if not bound.
|
||||||
if binds
|
if binds.iter().any(|bind| bind.action == Action::Screenshot) {
|
||||||
.iter()
|
|
||||||
.any(|bind| bind.actions.first() == Some(&Action::Screenshot))
|
|
||||||
{
|
|
||||||
actions.push(&Action::Screenshot);
|
actions.push(&Action::Screenshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the spawn actions.
|
// Add the spawn actions.
|
||||||
for bind in binds
|
let mut spawn_actions = Vec::new();
|
||||||
.iter()
|
for bind in binds.iter().filter(|bind| {
|
||||||
.filter(|bind| matches!(bind.actions.first(), Some(Action::Spawn(_))))
|
matches!(bind.action, Action::Spawn(_))
|
||||||
{
|
// Only show binds with Mod or Super to filter out stuff like volume up/down.
|
||||||
actions.push(bind.actions.first().unwrap());
|
&& (bind.key.modifiers.contains(Modifiers::COMPOSITOR)
|
||||||
|
|| bind.key.modifiers.contains(Modifiers::SUPER))
|
||||||
|
// Also filter out wheel and touchpad scroll binds.
|
||||||
|
&& matches!(bind.key.trigger, Trigger::Keysym(_))
|
||||||
|
}) {
|
||||||
|
let action = &bind.action;
|
||||||
|
|
||||||
|
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
|
||||||
|
if !spawn_actions.contains(&action) {
|
||||||
|
spawn_actions.push(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
actions.extend(spawn_actions);
|
||||||
|
|
||||||
let strings = actions
|
let strings = actions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -230,7 +245,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
.binds
|
.binds
|
||||||
.0
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.find(|bind| bind.actions.first() == Some(action))
|
.find(|bind| bind.action == *action)
|
||||||
.map(|bind| key_name(comp_mod, &bind.key))
|
.map(|bind| key_name(comp_mod, &bind.key))
|
||||||
.unwrap_or_else(|| String::from("(not bound)"));
|
.unwrap_or_else(|| String::from("(not bound)"));
|
||||||
|
|
||||||
@@ -243,7 +258,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
|
|
||||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||||
let cr = cairo::Context::new(&surface)?;
|
let cr = cairo::Context::new(&surface)?;
|
||||||
let layout = pangocairo::create_layout(&cr);
|
let layout = pangocairo::functions::create_layout(&cr);
|
||||||
layout.set_font_description(Some(&font));
|
layout.set_font_description(Some(&font));
|
||||||
|
|
||||||
let bold = AttrList::new();
|
let bold = AttrList::new();
|
||||||
@@ -298,7 +313,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
cr.paint()?;
|
cr.paint()?;
|
||||||
|
|
||||||
cr.move_to(padding.into(), padding.into());
|
cr.move_to(padding.into(), padding.into());
|
||||||
let layout = pangocairo::create_layout(&cr);
|
let layout = pangocairo::functions::create_layout(&cr);
|
||||||
layout.set_font_description(Some(&font));
|
layout.set_font_description(Some(&font));
|
||||||
|
|
||||||
cr.set_source_rgb(1., 1., 1.);
|
cr.set_source_rgb(1., 1., 1.);
|
||||||
@@ -306,20 +321,20 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
|
cr.move_to(((width - title_size.0) / 2).into(), padding.into());
|
||||||
layout.set_attributes(Some(&bold));
|
layout.set_attributes(Some(&bold));
|
||||||
layout.set_text(TITLE);
|
layout.set_text(TITLE);
|
||||||
pangocairo::show_layout(&cr, &layout);
|
pangocairo::functions::show_layout(&cr, &layout);
|
||||||
|
|
||||||
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
|
cr.move_to(padding.into(), (padding + title_size.1 + padding).into());
|
||||||
|
|
||||||
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
|
for ((key, action), ((_, key_h), (_, act_h))) in zip(&strings, zip(&key_sizes, &action_sizes)) {
|
||||||
layout.set_attributes(Some(&attrs));
|
layout.set_attributes(Some(&attrs));
|
||||||
layout.set_text(key);
|
layout.set_text(key);
|
||||||
pangocairo::show_layout(&cr, &layout);
|
pangocairo::functions::show_layout(&cr, &layout);
|
||||||
|
|
||||||
cr.rel_move_to((key_width + padding).into(), 0.);
|
cr.rel_move_to((key_width + padding).into(), 0.);
|
||||||
|
|
||||||
layout.set_attributes(None);
|
layout.set_attributes(None);
|
||||||
layout.set_markup(action);
|
layout.set_markup(action);
|
||||||
pangocairo::show_layout(&cr, &layout);
|
pangocairo::functions::show_layout(&cr, &layout);
|
||||||
|
|
||||||
cr.rel_move_to(
|
cr.rel_move_to(
|
||||||
(-(key_width + padding)).into(),
|
(-(key_width + padding)).into(),
|
||||||
@@ -338,7 +353,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
drop(cr);
|
drop(cr);
|
||||||
|
|
||||||
let data = surface.take_data().unwrap();
|
let data = surface.take_data().unwrap();
|
||||||
let buffer = MemoryRenderBuffer::from_memory(
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
&data,
|
&data,
|
||||||
Fourcc::Argb8888,
|
Fourcc::Argb8888,
|
||||||
(width, height),
|
(width, height),
|
||||||
@@ -356,7 +371,7 @@ fn render(config: &Config, comp_mod: CompositorMod, scale: i32) -> anyhow::Resul
|
|||||||
|
|
||||||
fn action_name(action: &Action) -> String {
|
fn action_name(action: &Action) -> String {
|
||||||
match action {
|
match action {
|
||||||
Action::Quit => String::from("Exit niri"),
|
Action::Quit(_) => String::from("Exit niri"),
|
||||||
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
|
Action::ShowHotkeyOverlay => String::from("Show Important Hotkeys"),
|
||||||
Action::CloseWindow => String::from("Close Focused Window"),
|
Action::CloseWindow => String::from("Close Focused Window"),
|
||||||
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
|
Action::FocusColumnLeft => String::from("Focus Column to the Left"),
|
||||||
@@ -401,7 +416,19 @@ fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
|
|||||||
if key.modifiers.contains(Modifiers::CTRL) {
|
if key.modifiers.contains(Modifiers::CTRL) {
|
||||||
name.push_str("Ctrl + ");
|
name.push_str("Ctrl + ");
|
||||||
}
|
}
|
||||||
name.push_str(&prettify_keysym_name(&keysym_get_name(key.keysym)));
|
|
||||||
|
let pretty = match key.trigger {
|
||||||
|
Trigger::Keysym(keysym) => prettify_keysym_name(&keysym_get_name(keysym)),
|
||||||
|
Trigger::WheelScrollDown => String::from("Wheel Scroll Down"),
|
||||||
|
Trigger::WheelScrollUp => String::from("Wheel Scroll Up"),
|
||||||
|
Trigger::WheelScrollLeft => String::from("Wheel Scroll Left"),
|
||||||
|
Trigger::WheelScrollRight => String::from("Wheel Scroll Right"),
|
||||||
|
Trigger::TouchpadScrollDown => String::from("Touchpad Scroll Down"),
|
||||||
|
Trigger::TouchpadScrollUp => String::from("Touchpad Scroll Up"),
|
||||||
|
Trigger::TouchpadScrollLeft => String::from("Touchpad Scroll Left"),
|
||||||
|
Trigger::TouchpadScrollRight => String::from("Touchpad Scroll Right"),
|
||||||
|
};
|
||||||
|
name.push_str(&pretty);
|
||||||
|
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod config_error_notification;
|
||||||
|
pub mod exit_confirm_dialog;
|
||||||
|
pub mod hotkey_overlay;
|
||||||
|
pub mod screenshot_ui;
|
||||||
@@ -10,16 +10,16 @@ use smithay::backend::allocator::Fourcc;
|
|||||||
use smithay::backend::input::{ButtonState, MouseButton};
|
use smithay::backend::input::{ButtonState, MouseButton};
|
||||||
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement};
|
||||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
use smithay::backend::renderer::element::Kind;
|
||||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
use smithay::backend::renderer::utils::CommitCounter;
|
|
||||||
use smithay::backend::renderer::ExportMem;
|
use smithay::backend::renderer::ExportMem;
|
||||||
use smithay::input::keyboard::{Keysym, ModifiersState};
|
use smithay::input::keyboard::{Keysym, ModifiersState};
|
||||||
use smithay::output::{Output, WeakOutput};
|
use smithay::output::{Output, WeakOutput};
|
||||||
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
|
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
|
||||||
|
|
||||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
use crate::niri_render_elements;
|
||||||
use crate::render_helpers::PrimaryGpuTextureRenderElement;
|
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||||
|
use crate::render_helpers::RenderTarget;
|
||||||
|
|
||||||
const BORDER: i32 = 2;
|
const BORDER: i32 = 2;
|
||||||
|
|
||||||
@@ -42,16 +42,19 @@ pub enum ScreenshotUi {
|
|||||||
pub struct OutputData {
|
pub struct OutputData {
|
||||||
size: Size<i32, Physical>,
|
size: Size<i32, Physical>,
|
||||||
scale: i32,
|
scale: i32,
|
||||||
texture: GlesTexture,
|
transform: Transform,
|
||||||
texture_buffer: TextureBuffer<GlesTexture>,
|
// Output, screencast, screen capture.
|
||||||
|
texture: [GlesTexture; 3],
|
||||||
|
texture_buffer: [TextureBuffer<GlesTexture>; 3],
|
||||||
buffers: [SolidColorBuffer; 8],
|
buffers: [SolidColorBuffer; 8],
|
||||||
locations: [Point<i32, Physical>; 8],
|
locations: [Point<i32, Physical>; 8],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
niri_render_elements! {
|
||||||
pub enum ScreenshotUiRenderElement {
|
ScreenshotUiRenderElement => {
|
||||||
Screenshot(PrimaryGpuTextureRenderElement),
|
Screenshot = PrimaryGpuTextureRenderElement,
|
||||||
SolidColor(SolidColorRenderElement),
|
SolidColor = SolidColorRenderElement,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScreenshotUi {
|
impl ScreenshotUi {
|
||||||
@@ -64,7 +67,8 @@ impl ScreenshotUi {
|
|||||||
pub fn open(
|
pub fn open(
|
||||||
&mut self,
|
&mut self,
|
||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
screenshots: HashMap<Output, GlesTexture>,
|
// Output, screencast, screen capture.
|
||||||
|
screenshots: HashMap<Output, [GlesTexture; 3]>,
|
||||||
default_output: Output,
|
default_output: Output,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if screenshots.is_empty() {
|
if screenshots.is_empty() {
|
||||||
@@ -94,6 +98,7 @@ impl ScreenshotUi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let scale = selection.0.current_scale().integer_scale();
|
let scale = selection.0.current_scale().integer_scale();
|
||||||
let selection = (
|
let selection = (
|
||||||
selection.0,
|
selection.0,
|
||||||
@@ -104,17 +109,13 @@ impl ScreenshotUi {
|
|||||||
let output_data = screenshots
|
let output_data = screenshots
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(output, texture)| {
|
.map(|(output, texture)| {
|
||||||
let output_transform = output.current_transform();
|
let transform = output.current_transform();
|
||||||
let output_mode = output.current_mode().unwrap();
|
let output_mode = output.current_mode().unwrap();
|
||||||
let size = output_transform.transform_size(output_mode.size);
|
let size = transform.transform_size(output_mode.size);
|
||||||
let scale = output.current_scale().integer_scale();
|
let scale = output.current_scale().integer_scale();
|
||||||
let texture_buffer = TextureBuffer::from_texture(
|
let texture_buffer = texture.clone().map(|texture| {
|
||||||
renderer,
|
TextureBuffer::from_texture(renderer, texture, scale, Transform::Normal, None)
|
||||||
texture.clone(),
|
});
|
||||||
scale,
|
|
||||||
Transform::Normal,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let buffers = [
|
let buffers = [
|
||||||
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
|
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
|
||||||
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
|
SolidColorBuffer::new((0, 0), [1., 1., 1., 1.]),
|
||||||
@@ -129,6 +130,7 @@ impl ScreenshotUi {
|
|||||||
let data = OutputData {
|
let data = OutputData {
|
||||||
size,
|
size,
|
||||||
scale,
|
scale,
|
||||||
|
transform,
|
||||||
texture,
|
texture,
|
||||||
texture_buffer,
|
texture_buffer,
|
||||||
buffers,
|
buffers,
|
||||||
@@ -240,7 +242,11 @@ impl ScreenshotUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_output(&self, output: &Output) -> ArrayVec<ScreenshotUiRenderElement, 9> {
|
pub fn render_output(
|
||||||
|
&self,
|
||||||
|
output: &Output,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> ArrayVec<ScreenshotUiRenderElement, 9> {
|
||||||
let _span = tracy_client::span!("ScreenshotUi::render_output");
|
let _span = tracy_client::span!("ScreenshotUi::render_output");
|
||||||
|
|
||||||
let Self::Open { output_data, .. } = self else {
|
let Self::Open { output_data, .. } = self else {
|
||||||
@@ -266,10 +272,15 @@ impl ScreenshotUi {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// The screenshot itself goes last.
|
// The screenshot itself goes last.
|
||||||
|
let index = match target {
|
||||||
|
RenderTarget::Output => 0,
|
||||||
|
RenderTarget::Screencast => 1,
|
||||||
|
RenderTarget::ScreenCapture => 2,
|
||||||
|
};
|
||||||
elements.push(
|
elements.push(
|
||||||
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||||
(0., 0.),
|
(0., 0.),
|
||||||
&output_data.texture_buffer,
|
&output_data.texture_buffer[index],
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -304,7 +315,7 @@ impl ScreenshotUi {
|
|||||||
.to_buffer(1, Transform::Normal, &data.size.to_logical(1));
|
.to_buffer(1, Transform::Normal, &data.size.to_logical(1));
|
||||||
|
|
||||||
let mapping = renderer
|
let mapping = renderer
|
||||||
.copy_texture(&data.texture, buf_rect, Fourcc::Abgr8888)
|
.copy_texture(&data.texture[0], buf_rect, Fourcc::Abgr8888)
|
||||||
.context("error copying texture")?;
|
.context("error copying texture")?;
|
||||||
let copy = renderer
|
let copy = renderer
|
||||||
.map_texture(&mapping)
|
.map_texture(&mapping)
|
||||||
@@ -313,12 +324,12 @@ impl ScreenshotUi {
|
|||||||
Ok((rect.size, copy.to_vec()))
|
Ok((rect.size, copy.to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action(&self, raw: Option<Keysym>, mods: ModifiersState) -> Option<Action> {
|
pub fn action(&self, raw: Keysym, mods: ModifiersState) -> Option<Action> {
|
||||||
if !matches!(self, Self::Open { .. }) {
|
if !matches!(self, Self::Open { .. }) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
action(raw?, mods)
|
action(raw, mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selection_output(&self) -> Option<&Output> {
|
pub fn selection_output(&self) -> Option<&Output> {
|
||||||
@@ -333,10 +344,10 @@ impl ScreenshotUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32)> {
|
pub fn output_size(&self, output: &Output) -> Option<(Size<i32, Physical>, i32, Transform)> {
|
||||||
if let Self::Open { output_data, .. } = self {
|
if let Self::Open { output_data, .. } = self {
|
||||||
let data = output_data.get(output)?;
|
let data = output_data.get(output)?;
|
||||||
Some((data.size, data.scale))
|
Some((data.size, data.scale, data.transform))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -448,138 +459,3 @@ pub fn rect_from_corner_points(
|
|||||||
let y2 = max(a.y, b.y);
|
let y2 = max(a.y, b.y);
|
||||||
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
|
Rectangle::from_extemities((x1, y1), (x2 + scale, y2 + scale))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual RenderElement implementation due to AsGlesFrame requirement.
|
|
||||||
impl Element for ScreenshotUiRenderElement {
|
|
||||||
fn id(&self) -> &Id {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.id(),
|
|
||||||
Self::SolidColor(elem) => elem.id(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_commit(&self) -> CommitCounter {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.current_commit(),
|
|
||||||
Self::SolidColor(elem) => elem.current_commit(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.geometry(scale),
|
|
||||||
Self::SolidColor(elem) => elem.geometry(scale),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transform(&self) -> Transform {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.transform(),
|
|
||||||
Self::SolidColor(elem) => elem.transform(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.src(),
|
|
||||||
Self::SolidColor(elem) => elem.src(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn damage_since(
|
|
||||||
&self,
|
|
||||||
scale: Scale<f64>,
|
|
||||||
commit: Option<CommitCounter>,
|
|
||||||
) -> Vec<Rectangle<i32, Physical>> {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.damage_since(scale, commit),
|
|
||||||
Self::SolidColor(elem) => elem.damage_since(scale, commit),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn opaque_regions(&self, scale: Scale<f64>) -> Vec<Rectangle<i32, Physical>> {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.opaque_regions(scale),
|
|
||||||
Self::SolidColor(elem) => elem.opaque_regions(scale),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn alpha(&self) -> f32 {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.alpha(),
|
|
||||||
Self::SolidColor(elem) => elem.alpha(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kind(&self) -> Kind {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => elem.kind(),
|
|
||||||
Self::SolidColor(elem) => elem.kind(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderElement<GlesRenderer> for ScreenshotUiRenderElement {
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
frame: &mut GlesFrame<'_>,
|
|
||||||
src: Rectangle<f64, Buffer>,
|
|
||||||
dst: Rectangle<i32, Physical>,
|
|
||||||
damage: &[Rectangle<i32, Physical>],
|
|
||||||
) -> Result<(), GlesError> {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => {
|
|
||||||
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
|
||||||
}
|
|
||||||
Self::SolidColor(elem) => {
|
|
||||||
RenderElement::<GlesRenderer>::draw(&elem, frame, src, dst, damage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 'alloc> RenderElement<TtyRenderer<'render, 'alloc>> for ScreenshotUiRenderElement {
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
frame: &mut TtyFrame<'render, 'alloc, '_>,
|
|
||||||
src: Rectangle<f64, Buffer>,
|
|
||||||
dst: Rectangle<i32, Physical>,
|
|
||||||
damage: &[Rectangle<i32, Physical>],
|
|
||||||
) -> Result<(), TtyRendererError<'render, 'alloc>> {
|
|
||||||
match self {
|
|
||||||
Self::Screenshot(elem) => {
|
|
||||||
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
|
||||||
}
|
|
||||||
Self::SolidColor(elem) => {
|
|
||||||
RenderElement::<TtyRenderer<'render, 'alloc>>::draw(&elem, frame, src, dst, damage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn underlying_storage(
|
|
||||||
&self,
|
|
||||||
_renderer: &mut TtyRenderer<'render, 'alloc>,
|
|
||||||
) -> Option<UnderlyingStorage> {
|
|
||||||
// If scanout for things other than Wayland buffers is implemented, this will need to take
|
|
||||||
// the target GPU into account.
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SolidColorRenderElement> for ScreenshotUiRenderElement {
|
|
||||||
fn from(x: SolidColorRenderElement) -> Self {
|
|
||||||
Self::SolidColor(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PrimaryGpuTextureRenderElement> for ScreenshotUiRenderElement {
|
|
||||||
fn from(x: PrimaryGpuTextureRenderElement) -> Self {
|
|
||||||
Self::Screenshot(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
|
/// Counter that returns unique IDs.
|
||||||
|
///
|
||||||
|
/// Under the hood it uses a `u32` that will eventually wrap around. When incrementing it once a
|
||||||
|
/// second, it will wrap around after about 136 years.
|
||||||
|
pub struct IdCounter {
|
||||||
|
value: AtomicU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdCounter {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
value: AtomicU32::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> u32 {
|
||||||
|
self.value.fetch_add(1, Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IdCounter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,31 @@
|
|||||||
use std::ffi::{CString, OsStr};
|
use std::ffi::{CString, OsStr};
|
||||||
use std::io::{self, Write};
|
use std::io::Write;
|
||||||
use std::os::unix::prelude::OsStrExt;
|
use std::os::unix::prelude::OsStrExt;
|
||||||
use std::os::unix::process::CommandExt;
|
use std::path::{Path, PathBuf};
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::ptr::null_mut;
|
use std::ptr::null_mut;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{ensure, Context};
|
use anyhow::{ensure, Context};
|
||||||
use directories::UserDirs;
|
use directories::UserDirs;
|
||||||
|
use git_version::git_version;
|
||||||
use niri_config::Config;
|
use niri_config::Config;
|
||||||
use smithay::output::Output;
|
use smithay::output::Output;
|
||||||
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
|
||||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Size, Transform};
|
||||||
|
|
||||||
pub fn clone2<T: Clone, U: Clone>(t: (&T, &U)) -> (T, U) {
|
pub mod id;
|
||||||
(t.0.clone(), t.1.clone())
|
pub mod spawning;
|
||||||
|
pub mod watcher;
|
||||||
|
|
||||||
|
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub fn version() -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
git_version!(fallback = "unknown commit"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_monotonic_time() -> Duration {
|
pub fn get_monotonic_time() -> Duration {
|
||||||
@@ -29,6 +37,10 @@ pub fn center(rect: Rectangle<i32, Logical>) -> Point<i32, Logical> {
|
|||||||
rect.loc + rect.size.downscale(2).to_point()
|
rect.loc + rect.size.downscale(2).to_point()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn center_f64(rect: Rectangle<f64, Logical>) -> Point<f64, Logical> {
|
||||||
|
rect.loc + rect.size.downscale(2.0).to_point()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn output_size(output: &Output) -> Size<i32, Logical> {
|
pub fn output_size(output: &Output) -> Size<i32, Logical> {
|
||||||
let output_scale = output.current_scale().integer_scale();
|
let output_scale = output.current_scale().integer_scale();
|
||||||
let output_transform = output.current_transform();
|
let output_transform = output.current_transform();
|
||||||
@@ -39,6 +51,51 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
|
|||||||
.to_logical(output_scale)
|
.to_logical(output_scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn logical_output(output: &Output) -> niri_ipc::LogicalOutput {
|
||||||
|
let loc = output.current_location();
|
||||||
|
let size = output_size(output);
|
||||||
|
let transform = match output.current_transform() {
|
||||||
|
Transform::Normal => niri_ipc::Transform::Normal,
|
||||||
|
Transform::_90 => niri_ipc::Transform::_90,
|
||||||
|
Transform::_180 => niri_ipc::Transform::_180,
|
||||||
|
Transform::_270 => niri_ipc::Transform::_270,
|
||||||
|
Transform::Flipped => niri_ipc::Transform::Flipped,
|
||||||
|
Transform::Flipped90 => niri_ipc::Transform::Flipped90,
|
||||||
|
Transform::Flipped180 => niri_ipc::Transform::Flipped180,
|
||||||
|
Transform::Flipped270 => niri_ipc::Transform::Flipped270,
|
||||||
|
};
|
||||||
|
niri_ipc::LogicalOutput {
|
||||||
|
x: loc.x,
|
||||||
|
y: loc.y,
|
||||||
|
width: size.w as u32,
|
||||||
|
height: size.h as u32,
|
||||||
|
scale: output.current_scale().fractional_scale(),
|
||||||
|
transform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipc_transform_to_smithay(transform: niri_ipc::Transform) -> Transform {
|
||||||
|
match transform {
|
||||||
|
niri_ipc::Transform::Normal => Transform::Normal,
|
||||||
|
niri_ipc::Transform::_90 => Transform::_90,
|
||||||
|
niri_ipc::Transform::_180 => Transform::_180,
|
||||||
|
niri_ipc::Transform::_270 => Transform::_270,
|
||||||
|
niri_ipc::Transform::Flipped => Transform::Flipped,
|
||||||
|
niri_ipc::Transform::Flipped90 => Transform::Flipped90,
|
||||||
|
niri_ipc::Transform::Flipped180 => Transform::Flipped180,
|
||||||
|
niri_ipc::Transform::Flipped270 => Transform::Flipped270,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
|
||||||
|
if let Ok(rest) = path.strip_prefix("~") {
|
||||||
|
let dirs = UserDirs::new().context("error retrieving home directory")?;
|
||||||
|
Ok(Some([dirs.home_dir(), rest].iter().collect()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
|
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
|
||||||
let Some(path) = &config.screenshot_path else {
|
let Some(path) = &config.screenshot_path else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -61,91 +118,13 @@ pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>>
|
|||||||
path = PathBuf::from(OsStr::from_bytes(&buf[..rv]));
|
path = PathBuf::from(OsStr::from_bytes(&buf[..rv]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(rest) = path.strip_prefix("~") {
|
if let Some(expanded) = expand_home(&path).context("error expanding ~")? {
|
||||||
let dirs = UserDirs::new().context("error retrieving home directory")?;
|
path = expanded;
|
||||||
path = [dirs.home_dir(), rest].iter().collect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(path))
|
Ok(Some(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
|
||||||
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
|
||||||
|
|
||||||
/// Spawns the command to run independently of the compositor.
|
|
||||||
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
|
|
||||||
let _span = tracy_client::span!();
|
|
||||||
|
|
||||||
if command.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawning and waiting takes some milliseconds, so do it in a thread.
|
|
||||||
let res = thread::Builder::new()
|
|
||||||
.name("Command Spawner".to_owned())
|
|
||||||
.spawn(move || {
|
|
||||||
let (command, args) = command.split_first().unwrap();
|
|
||||||
spawn_sync(command, args);
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
warn!("error spawning a thread to spawn the command: {err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
|
|
||||||
let _span = tracy_client::span!();
|
|
||||||
|
|
||||||
let command = command.as_ref();
|
|
||||||
|
|
||||||
let mut process = Command::new(command);
|
|
||||||
process
|
|
||||||
.args(args)
|
|
||||||
.stdin(Stdio::null())
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null());
|
|
||||||
|
|
||||||
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
|
|
||||||
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
|
|
||||||
process.env_remove("RUST_BACKTRACE");
|
|
||||||
}
|
|
||||||
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
|
|
||||||
process.env_remove("RUST_LIB_BACKTRACE");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double-fork to avoid having to waitpid the child.
|
|
||||||
unsafe {
|
|
||||||
process.pre_exec(|| {
|
|
||||||
match libc::fork() {
|
|
||||||
-1 => return Err(io::Error::last_os_error()),
|
|
||||||
0 => (),
|
|
||||||
_ => libc::_exit(0),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut child = match process.spawn() {
|
|
||||||
Ok(child) => child,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("error spawning {command:?}: {err:?}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match child.wait() {
|
|
||||||
Ok(status) => {
|
|
||||||
if !status.success() {
|
|
||||||
warn!("child did not exit successfully: {status:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("error waiting for child: {err:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_png_rgba8(
|
pub fn write_png_rgba8(
|
||||||
w: impl Write,
|
w: impl Write,
|
||||||
width: u32,
|
width: u32,
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::{io, thread};
|
||||||
|
|
||||||
|
use niri_config::Environment;
|
||||||
|
|
||||||
|
use crate::utils::expand_home;
|
||||||
|
|
||||||
|
pub static REMOVE_ENV_RUST_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
||||||
|
pub static REMOVE_ENV_RUST_LIB_BACKTRACE: AtomicBool = AtomicBool::new(false);
|
||||||
|
pub static CHILD_ENV: RwLock<Environment> = RwLock::new(Environment(Vec::new()));
|
||||||
|
|
||||||
|
/// Spawns the command to run independently of the compositor.
|
||||||
|
pub fn spawn<T: AsRef<OsStr> + Send + 'static>(command: Vec<T>) {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
if command.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawning and waiting takes some milliseconds, so do it in a thread.
|
||||||
|
let res = thread::Builder::new()
|
||||||
|
.name("Command Spawner".to_owned())
|
||||||
|
.spawn(move || {
|
||||||
|
let (command, args) = command.split_first().unwrap();
|
||||||
|
spawn_sync(command, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
warn!("error spawning a thread to spawn the command: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_sync(command: impl AsRef<OsStr>, args: impl IntoIterator<Item = impl AsRef<OsStr>>) {
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
let mut command = command.as_ref();
|
||||||
|
|
||||||
|
// Expand `~` at the start.
|
||||||
|
let expanded = expand_home(Path::new(command));
|
||||||
|
match &expanded {
|
||||||
|
Ok(Some(expanded)) => command = expanded.as_ref(),
|
||||||
|
Ok(None) => (),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error expanding ~: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut process = Command::new(command);
|
||||||
|
process
|
||||||
|
.args(args)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null());
|
||||||
|
|
||||||
|
// Remove RUST_BACKTRACE and RUST_LIB_BACKTRACE from the environment if needed.
|
||||||
|
if REMOVE_ENV_RUST_BACKTRACE.load(Ordering::Relaxed) {
|
||||||
|
process.env_remove("RUST_BACKTRACE");
|
||||||
|
}
|
||||||
|
if REMOVE_ENV_RUST_LIB_BACKTRACE.load(Ordering::Relaxed) {
|
||||||
|
process.env_remove("RUST_LIB_BACKTRACE");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set configured environment.
|
||||||
|
let env = CHILD_ENV.read().unwrap();
|
||||||
|
for var in &env.0 {
|
||||||
|
if let Some(value) = &var.value {
|
||||||
|
process.env(&var.name, value);
|
||||||
|
} else {
|
||||||
|
process.env_remove(&var.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(env);
|
||||||
|
|
||||||
|
let Some(mut child) = do_spawn(command, process) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match child.wait() {
|
||||||
|
Ok(status) => {
|
||||||
|
if !status.success() {
|
||||||
|
warn!("child did not exit successfully: {status:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error waiting for child: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "systemd"))]
|
||||||
|
fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
|
||||||
|
unsafe {
|
||||||
|
// Double-fork to avoid having to waitpid the child.
|
||||||
|
process.pre_exec(move || {
|
||||||
|
match libc::fork() {
|
||||||
|
-1 => return Err(io::Error::last_os_error()),
|
||||||
|
0 => (),
|
||||||
|
_ => libc::_exit(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = match process.spawn() {
|
||||||
|
Ok(child) => child,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error spawning {command:?}: {err:?}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
|
use systemd::do_spawn;
|
||||||
|
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
|
mod systemd {
|
||||||
|
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
|
||||||
|
|
||||||
|
use smithay::reexports::rustix;
|
||||||
|
use smithay::reexports::rustix::io::{close, read, retry_on_intr, write};
|
||||||
|
use smithay::reexports::rustix::pipe::{pipe_with, PipeFlags};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn do_spawn(command: &OsStr, mut process: Command) -> Option<Child> {
|
||||||
|
use libc::close_range;
|
||||||
|
|
||||||
|
// When running as a systemd session, we want to put children into their own transient
|
||||||
|
// scopes in order to separate them from the niri process. This is helpful for
|
||||||
|
// example to prevent the OOM killer from taking down niri together with a
|
||||||
|
// misbehaving client.
|
||||||
|
//
|
||||||
|
// Putting a child into a scope is done by calling systemd's StartTransientUnit D-Bus method
|
||||||
|
// with a PID. Unfortunately, there seems to be a race in systemd where if the child exits
|
||||||
|
// at just the right time, the transient unit will be created but empty, so it will
|
||||||
|
// linger around forever.
|
||||||
|
//
|
||||||
|
// To prevent this, we'll use our double-fork (done for a separate reason) to help. In our
|
||||||
|
// intermediate child we will send back the grandchild PID, and in niri we will create a
|
||||||
|
// transient scope with both our intermediate child and the grandchild PIDs set. Only then
|
||||||
|
// we will signal our intermediate child to exit. This way, even if the grandchild
|
||||||
|
// exits quickly, a non-empty scope will be created (with just our intermediate
|
||||||
|
// child), then cleaned up when our intermediate child exits.
|
||||||
|
|
||||||
|
// Make a pipe to receive the grandchild PID.
|
||||||
|
|
||||||
|
let (pipe_pid_read, pipe_pid_write) = pipe_with(PipeFlags::CLOEXEC)
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!("error creating a pipe to transfer child PID: {err:?}");
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.unzip();
|
||||||
|
// Make a pipe to wait in the intermediate child.
|
||||||
|
let (pipe_wait_read, pipe_wait_write) = pipe_with(PipeFlags::CLOEXEC)
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!("error creating a pipe for child to wait on: {err:?}");
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// The fds will be duplicated after a fork and closed on exec or exit automatically. Get
|
||||||
|
// the raw fd inside so that it's not closed any extra times.
|
||||||
|
let mut pipe_pid_read_fd = pipe_pid_read.as_ref().map(|fd| fd.as_raw_fd());
|
||||||
|
let mut pipe_pid_write_fd = pipe_pid_write.as_ref().map(|fd| fd.as_raw_fd());
|
||||||
|
let mut pipe_wait_read_fd = pipe_wait_read.as_ref().map(|fd| fd.as_raw_fd());
|
||||||
|
let mut pipe_wait_write_fd = pipe_wait_write.as_ref().map(|fd| fd.as_raw_fd());
|
||||||
|
|
||||||
|
// Double-fork to avoid having to waitpid the child.
|
||||||
|
process.pre_exec(move || {
|
||||||
|
// Close FDs that we don't need. Especially important for the write ones to unblock
|
||||||
|
// the readers.
|
||||||
|
if let Some(fd) = pipe_pid_read_fd.take() {
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
if let Some(fd) = pipe_wait_write_fd.take() {
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the our FDs to OwnedFd, which will close them in all of our fork paths.
|
||||||
|
let pipe_pid_write = pipe_pid_write_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
|
||||||
|
let pipe_wait_read = pipe_wait_read_fd.take().map(|fd| OwnedFd::from_raw_fd(fd));
|
||||||
|
|
||||||
|
match libc::fork() {
|
||||||
|
-1 => return Err(io::Error::last_os_error()),
|
||||||
|
0 => (),
|
||||||
|
grandchild_pid => {
|
||||||
|
// Send back the PID.
|
||||||
|
if let Some(pipe) = pipe_pid_write {
|
||||||
|
let _ = write_all(pipe, &grandchild_pid.to_ne_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the parent signals us to exit.
|
||||||
|
if let Some(pipe) = pipe_wait_read {
|
||||||
|
// We're going to exit afterwards. Close all other FDs to allow
|
||||||
|
// Command::spawn() to return in the parent process.
|
||||||
|
let raw = pipe.as_raw_fd() as u32;
|
||||||
|
let _ = close_range(0, raw - 1, 0);
|
||||||
|
let _ = close_range(raw + 1, !0, 0);
|
||||||
|
|
||||||
|
let _ = read_all(pipe, &mut [0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
libc::_exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = match process.spawn() {
|
||||||
|
Ok(child) => child,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error spawning {command:?}: {err:?}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drop(pipe_pid_write);
|
||||||
|
drop(pipe_wait_read);
|
||||||
|
|
||||||
|
// Wait for the grandchild PID.
|
||||||
|
if let Some(pipe) = pipe_pid_read {
|
||||||
|
let mut buf = [0; 4];
|
||||||
|
match read_all(pipe, &mut buf) {
|
||||||
|
Ok(()) => {
|
||||||
|
let pid = i32::from_ne_bytes(buf);
|
||||||
|
trace!("spawned PID: {pid}");
|
||||||
|
|
||||||
|
// Start a systemd scope for the grandchild.
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
|
if let Err(err) = start_systemd_scope(command, child.id(), pid as u32) {
|
||||||
|
trace!("error starting systemd scope for spawned command: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("error reading child PID: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal the intermediate child to exit now that we're done trying to creating a systemd
|
||||||
|
// scope.
|
||||||
|
trace!("signaling child to exit");
|
||||||
|
drop(pipe_wait_write);
|
||||||
|
|
||||||
|
Some(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
|
fn write_all(fd: impl AsFd, buf: &[u8]) -> rustix::io::Result<()> {
|
||||||
|
let mut written = 0;
|
||||||
|
loop {
|
||||||
|
let n = retry_on_intr(|| write(&fd, &buf[written..]))?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(rustix::io::Errno::CANCELED);
|
||||||
|
}
|
||||||
|
|
||||||
|
written += n;
|
||||||
|
if written == buf.len() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
|
fn read_all(fd: impl AsFd, buf: &mut [u8]) -> rustix::io::Result<()> {
|
||||||
|
let mut start = 0;
|
||||||
|
loop {
|
||||||
|
let n = retry_on_intr(|| read(&fd, &mut buf[start..]))?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(rustix::io::Errno::CANCELED);
|
||||||
|
}
|
||||||
|
|
||||||
|
start += n;
|
||||||
|
if start == buf.len() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a (newly spawned) pid into a transient systemd scope.
|
||||||
|
///
|
||||||
|
/// This separates the pid from the compositor scope, which for example prevents the OOM killer
|
||||||
|
/// from bringing down the compositor together with a misbehaving client.
|
||||||
|
#[cfg(feature = "systemd")]
|
||||||
|
fn start_systemd_scope(
|
||||||
|
name: &OsStr,
|
||||||
|
intermediate_pid: u32,
|
||||||
|
child_pid: u32,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use zbus::zvariant::{OwnedObjectPath, Value};
|
||||||
|
|
||||||
|
use crate::utils::IS_SYSTEMD_SERVICE;
|
||||||
|
|
||||||
|
// We only start transient scopes if we're a systemd service ourselves.
|
||||||
|
if !IS_SYSTEMD_SERVICE.load(Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _span = tracy_client::span!();
|
||||||
|
|
||||||
|
// Extract the basename.
|
||||||
|
let name = Path::new(name).file_name().unwrap_or(name);
|
||||||
|
|
||||||
|
let mut scope_name = String::from("app-niri-");
|
||||||
|
|
||||||
|
// Escape for systemd similarly to libgnome-desktop, which says it had adapted this from
|
||||||
|
// systemd source.
|
||||||
|
for &c in name.as_bytes() {
|
||||||
|
if c.is_ascii_alphanumeric() || matches!(c, b':' | b'_' | b'.') {
|
||||||
|
scope_name.push(char::from(c));
|
||||||
|
} else {
|
||||||
|
let _ = write!(scope_name, "\\x{c:02x}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = write!(scope_name, "-{child_pid}.scope");
|
||||||
|
|
||||||
|
// Ask systemd to start a transient scope.
|
||||||
|
static CONNECTION: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
|
||||||
|
let conn = CONNECTION
|
||||||
|
.get_or_init(zbus::blocking::Connection::session)
|
||||||
|
.clone()
|
||||||
|
.context("error connecting to session bus")?;
|
||||||
|
|
||||||
|
let proxy = zbus::blocking::Proxy::new(
|
||||||
|
&conn,
|
||||||
|
"org.freedesktop.systemd1",
|
||||||
|
"/org/freedesktop/systemd1",
|
||||||
|
"org.freedesktop.systemd1.Manager",
|
||||||
|
)
|
||||||
|
.context("error creating a Proxy")?;
|
||||||
|
|
||||||
|
let signals = proxy
|
||||||
|
.receive_signal("JobRemoved")
|
||||||
|
.context("error creating a signal iterator")?;
|
||||||
|
|
||||||
|
let pids: &[_] = &[intermediate_pid, child_pid];
|
||||||
|
let properties: &[_] = &[
|
||||||
|
("PIDs", Value::new(pids)),
|
||||||
|
("CollectMode", Value::new("inactive-or-failed")),
|
||||||
|
];
|
||||||
|
let aux: &[(&str, &[(&str, Value)])] = &[];
|
||||||
|
|
||||||
|
let job: OwnedObjectPath = proxy
|
||||||
|
.call("StartTransientUnit", &(scope_name, "fail", properties, aux))
|
||||||
|
.context("error calling StartTransientUnit")?;
|
||||||
|
|
||||||
|
trace!("waiting for JobRemoved");
|
||||||
|
for message in signals {
|
||||||
|
let body: (u32, OwnedObjectPath, &str, &str) =
|
||||||
|
message.body().context("error parsing signal")?;
|
||||||
|
|
||||||
|
if body.1 == job {
|
||||||
|
// Our transient unit had started, we're good to exit the intermediate child.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
//! File modification watcher.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{mpsc, Arc};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use smithay::reexports::calloop::channel::SyncSender;
|
||||||
|
|
||||||
|
pub struct Watcher {
|
||||||
|
should_stop: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Watcher {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.should_stop.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Watcher {
|
||||||
|
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
|
||||||
|
Self::with_start_notification(path, changed, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_start_notification(
|
||||||
|
path: PathBuf,
|
||||||
|
changed: SyncSender<()>,
|
||||||
|
started: Option<mpsc::SyncSender<()>>,
|
||||||
|
) -> Self {
|
||||||
|
let should_stop = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
{
|
||||||
|
let should_stop = should_stop.clone();
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
||||||
|
.spawn(move || {
|
||||||
|
// this "should" be as simple as mtime, but it does not quite work in practice;
|
||||||
|
// it doesn't work if the config is a symlink, and its target changes but the
|
||||||
|
// new target and old target have identical mtimes.
|
||||||
|
//
|
||||||
|
// in practice, this does not occur on any systems other than nix.
|
||||||
|
// because, on nix practically everything is a symlink to /nix/store
|
||||||
|
// and due to reproducibility, /nix/store keeps no mtime (= 1970-01-01)
|
||||||
|
// so, symlink targets change frequently when mtime doesn't.
|
||||||
|
let mut last_props = path
|
||||||
|
.canonicalize()
|
||||||
|
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if let Some(started) = started {
|
||||||
|
let _ = started.send(());
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
|
||||||
|
if should_stop.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(new_props) = path
|
||||||
|
.canonicalize()
|
||||||
|
.and_then(|canon| Ok((canon.metadata()?.modified()?, canon)))
|
||||||
|
{
|
||||||
|
if last_props.as_ref() != Some(&new_props) {
|
||||||
|
trace!("file changed: {}", path.to_string_lossy());
|
||||||
|
|
||||||
|
if let Err(err) = changed.send(()) {
|
||||||
|
warn!("error sending change notification: {err:?}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_props = Some(new_props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("exiting watcher thread for {}", path.to_string_lossy());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { should_stop }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::atomic::AtomicU8;
|
||||||
|
|
||||||
|
use calloop::channel::sync_channel;
|
||||||
|
use calloop::EventLoop;
|
||||||
|
use smithay::reexports::rustix::fs::{futimens, Timestamps};
|
||||||
|
use smithay::reexports::rustix::time::Timespec;
|
||||||
|
use xshell::{cmd, Shell};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn check(
|
||||||
|
setup: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
|
||||||
|
change: impl FnOnce(&Shell) -> Result<(), Box<dyn Error>>,
|
||||||
|
) {
|
||||||
|
let sh = Shell::new().unwrap();
|
||||||
|
let temp_dir = sh.create_temp_dir().unwrap();
|
||||||
|
sh.change_dir(temp_dir.path());
|
||||||
|
// let dir = sh.create_dir("xshell").unwrap();
|
||||||
|
// sh.change_dir(dir);
|
||||||
|
|
||||||
|
let mut config_path = sh.current_dir();
|
||||||
|
config_path.push("niri");
|
||||||
|
config_path.push("config.kdl");
|
||||||
|
|
||||||
|
setup(&sh).unwrap();
|
||||||
|
|
||||||
|
let changed = AtomicU8::new(0);
|
||||||
|
|
||||||
|
let mut event_loop = EventLoop::try_new().unwrap();
|
||||||
|
let loop_handle = event_loop.handle();
|
||||||
|
|
||||||
|
let (tx, rx) = sync_channel(1);
|
||||||
|
let (started_tx, started_rx) = mpsc::sync_channel(1);
|
||||||
|
let _watcher = Watcher::with_start_notification(config_path.clone(), tx, Some(started_tx));
|
||||||
|
loop_handle
|
||||||
|
.insert_source(rx, |_, _, _| {
|
||||||
|
changed.fetch_add(1, Ordering::SeqCst);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
started_rx.recv().unwrap();
|
||||||
|
|
||||||
|
// HACK: if we don't sleep, files might have the same mtime.
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
change(&sh).unwrap();
|
||||||
|
|
||||||
|
event_loop
|
||||||
|
.dispatch(Duration::from_millis(750), &mut ())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(changed.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
// Verify that the watcher didn't break.
|
||||||
|
sh.write_file(&config_path, "c").unwrap();
|
||||||
|
|
||||||
|
event_loop
|
||||||
|
.dispatch(Duration::from_millis(750), &mut ())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(changed.load(Ordering::SeqCst), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_file() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "a")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "b")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_file() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.create_dir("niri")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "a")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_dir_and_file() {
|
||||||
|
check(
|
||||||
|
|_sh| Ok(()),
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "a")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_linked_file() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config2.kdl", "a")?;
|
||||||
|
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config2.kdl", "b")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_file_in_linked_dir() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri2/config.kdl", "a")?;
|
||||||
|
cmd!(sh, "ln -s niri2 niri").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri2/config.kdl", "b")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recreate_file() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "a")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.remove_path("niri/config.kdl")?;
|
||||||
|
sh.write_file("niri/config.kdl", "b")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recreate_dir() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "a")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.remove_path("niri")?;
|
||||||
|
sh.write_file("niri/config.kdl", "b")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swap_dir() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri/config.kdl", "a")?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri2/config.kdl", "b")?;
|
||||||
|
sh.remove_path("niri")?;
|
||||||
|
cmd!(sh, "mv niri2 niri").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swap_just_link() {
|
||||||
|
// NixOS setup: link path changes, mtime stays constant.
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
let mut dir = sh.current_dir();
|
||||||
|
dir.push("niri");
|
||||||
|
sh.create_dir(&dir)?;
|
||||||
|
|
||||||
|
let mut d2 = dir.clone();
|
||||||
|
d2.push("config2.kdl");
|
||||||
|
let mut c2 = File::create(d2).unwrap();
|
||||||
|
write!(c2, "a")?;
|
||||||
|
c2.flush()?;
|
||||||
|
futimens(
|
||||||
|
&c2,
|
||||||
|
&Timestamps {
|
||||||
|
last_access: Timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
},
|
||||||
|
last_modification: Timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
c2.sync_all()?;
|
||||||
|
drop(c2);
|
||||||
|
|
||||||
|
let mut d3 = dir.clone();
|
||||||
|
d3.push("config3.kdl");
|
||||||
|
let mut c3 = File::create(d3).unwrap();
|
||||||
|
write!(c3, "b")?;
|
||||||
|
c3.flush()?;
|
||||||
|
futimens(
|
||||||
|
&c3,
|
||||||
|
&Timestamps {
|
||||||
|
last_access: Timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
},
|
||||||
|
last_modification: Timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
c3.sync_all()?;
|
||||||
|
drop(c3);
|
||||||
|
|
||||||
|
cmd!(sh, "ln -s config2.kdl niri/config.kdl").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
cmd!(sh, "unlink niri/config.kdl").run()?;
|
||||||
|
cmd!(sh, "ln -s config3.kdl niri/config.kdl").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swap_dir_link() {
|
||||||
|
check(
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri2/config.kdl", "a")?;
|
||||||
|
cmd!(sh, "ln -s niri2 niri").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
|sh| {
|
||||||
|
sh.write_file("niri3/config.kdl", "b")?;
|
||||||
|
cmd!(sh, "unlink niri").run()?;
|
||||||
|
cmd!(sh, "ln -s niri3 niri").run()?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
//! File modification watcher.
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use smithay::reexports::calloop::channel::SyncSender;
|
|
||||||
|
|
||||||
pub struct Watcher {
|
|
||||||
should_stop: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Watcher {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.should_stop.store(true, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Watcher {
|
|
||||||
pub fn new(path: PathBuf, changed: SyncSender<()>) -> Self {
|
|
||||||
let should_stop = Arc::new(AtomicBool::new(false));
|
|
||||||
|
|
||||||
{
|
|
||||||
let should_stop = should_stop.clone();
|
|
||||||
thread::Builder::new()
|
|
||||||
.name(format!("Filesystem Watcher for {}", path.to_string_lossy()))
|
|
||||||
.spawn(move || {
|
|
||||||
let mut last_mtime = path.metadata().and_then(|meta| meta.modified()).ok();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
thread::sleep(Duration::from_millis(500));
|
|
||||||
|
|
||||||
if should_stop.load(Ordering::SeqCst) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(mtime) = path.metadata().and_then(|meta| meta.modified()) {
|
|
||||||
if last_mtime != Some(mtime) {
|
|
||||||
trace!("file changed: {}", path.to_string_lossy());
|
|
||||||
|
|
||||||
if let Err(err) = changed.send(()) {
|
|
||||||
warn!("error sending change notification: {err:?}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
last_mtime = Some(mtime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("exiting watcher thread for {}", path.to_string_lossy());
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Self { should_stop }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::cmp::{max, min};
|
||||||
|
|
||||||
|
use niri_config::{BlockOutFrom, WindowRule};
|
||||||
|
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
|
||||||
|
use smithay::backend::renderer::element::{AsRenderElements, Id, Kind};
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
use smithay::desktop::space::SpaceElement as _;
|
||||||
|
use smithay::desktop::{PopupManager, Window};
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
|
||||||
|
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::wayland::compositor::{
|
||||||
|
remove_pre_commit_hook, send_surface_state, with_states, HookId,
|
||||||
|
};
|
||||||
|
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
|
||||||
|
|
||||||
|
use super::{ResolvedWindowRules, WindowRef};
|
||||||
|
use crate::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot};
|
||||||
|
use crate::niri::WindowOffscreenId;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||||
|
use crate::render_helpers::surface::render_snapshot_from_surface_tree;
|
||||||
|
use crate::render_helpers::{BakedBuffer, RenderTarget};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Mapped {
|
||||||
|
pub window: Window,
|
||||||
|
|
||||||
|
/// Pre-commit hook that we have on all mapped toplevel surfaces.
|
||||||
|
pre_commit_hook: HookId,
|
||||||
|
|
||||||
|
/// Up-to-date rules.
|
||||||
|
rules: ResolvedWindowRules,
|
||||||
|
|
||||||
|
/// Whether the window rules need to be recomputed.
|
||||||
|
///
|
||||||
|
/// This is not used in all cases; for example, app ID and title changes recompute the rules
|
||||||
|
/// immediately, rather than setting this flag.
|
||||||
|
need_to_recompute_rules: bool,
|
||||||
|
|
||||||
|
/// Whether this window has the keyboard focus.
|
||||||
|
is_focused: bool,
|
||||||
|
|
||||||
|
/// Buffer to draw instead of the window when it should be blocked out.
|
||||||
|
block_out_buffer: RefCell<SolidColorBuffer>,
|
||||||
|
|
||||||
|
/// Snapshot of the last render for use in the close animation.
|
||||||
|
unmap_snapshot: RefCell<Option<LayoutElementRenderSnapshot>>,
|
||||||
|
|
||||||
|
/// Whether the next configure should be animated, if the configured state changed.
|
||||||
|
animate_next_configure: bool,
|
||||||
|
|
||||||
|
/// Serials of commits that should be animated.
|
||||||
|
animate_serials: Vec<Serial>,
|
||||||
|
|
||||||
|
/// Snapshot right before an animated commit.
|
||||||
|
animation_snapshot: Option<LayoutElementRenderSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapped {
|
||||||
|
pub fn new(window: Window, rules: ResolvedWindowRules, hook: HookId) -> Self {
|
||||||
|
Self {
|
||||||
|
window,
|
||||||
|
pre_commit_hook: hook,
|
||||||
|
rules,
|
||||||
|
need_to_recompute_rules: false,
|
||||||
|
is_focused: false,
|
||||||
|
block_out_buffer: RefCell::new(SolidColorBuffer::new((0, 0), [0., 0., 0., 1.])),
|
||||||
|
unmap_snapshot: RefCell::new(None),
|
||||||
|
animate_next_configure: false,
|
||||||
|
animate_serials: Vec::new(),
|
||||||
|
animation_snapshot: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toplevel(&self) -> &ToplevelSurface {
|
||||||
|
self.window.toplevel().expect("no X11 support")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recomputes the resolved window rules and returns whether they changed.
|
||||||
|
pub fn recompute_window_rules(&mut self, rules: &[WindowRule]) -> bool {
|
||||||
|
self.need_to_recompute_rules = false;
|
||||||
|
|
||||||
|
let new_rules = ResolvedWindowRules::compute(rules, WindowRef::Mapped(self));
|
||||||
|
if new_rules == self.rules {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rules = new_rules;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recompute_window_rules_if_needed(&mut self, rules: &[WindowRule]) -> bool {
|
||||||
|
if !self.need_to_recompute_rules {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recompute_window_rules(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_focused(&self) -> bool {
|
||||||
|
self.is_focused
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_focused(&mut self, is_focused: bool) {
|
||||||
|
if self.is_focused == is_focused {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_focused = is_focused;
|
||||||
|
self.need_to_recompute_rules = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> LayoutElementRenderSnapshot {
|
||||||
|
let _span = tracy_client::span!("Mapped::render_snapshot");
|
||||||
|
|
||||||
|
let size = self.size();
|
||||||
|
|
||||||
|
let mut buffer = self.block_out_buffer.borrow_mut();
|
||||||
|
buffer.resize(size);
|
||||||
|
let blocked_out_contents = vec![BakedBuffer {
|
||||||
|
buffer: buffer.clone(),
|
||||||
|
location: Point::from((0, 0)),
|
||||||
|
src: None,
|
||||||
|
dst: None,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let buf_pos = self.window.geometry().loc.upscale(-1);
|
||||||
|
|
||||||
|
let mut contents = vec![];
|
||||||
|
|
||||||
|
let surface = self.toplevel().wl_surface();
|
||||||
|
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
|
||||||
|
let offset = self.window.geometry().loc + popup_offset - popup.geometry().loc;
|
||||||
|
|
||||||
|
render_snapshot_from_surface_tree(
|
||||||
|
renderer,
|
||||||
|
popup.wl_surface(),
|
||||||
|
buf_pos + offset,
|
||||||
|
&mut contents,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_snapshot_from_surface_tree(renderer, surface, buf_pos, &mut contents);
|
||||||
|
|
||||||
|
RenderSnapshot {
|
||||||
|
contents,
|
||||||
|
blocked_out_contents,
|
||||||
|
block_out_from: self.rules().block_out_from,
|
||||||
|
size,
|
||||||
|
texture: Default::default(),
|
||||||
|
blocked_out_texture: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_unmap_snapshot_if_empty(&self, renderer: &mut GlesRenderer) {
|
||||||
|
let mut snapshot = self.unmap_snapshot.borrow_mut();
|
||||||
|
if snapshot.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*snapshot = Some(self.render_snapshot(renderer));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_animate_commit(&mut self, commit_serial: Serial) -> bool {
|
||||||
|
let mut should_animate = false;
|
||||||
|
self.animate_serials.retain_mut(|serial| {
|
||||||
|
if commit_serial.is_no_older_than(serial) {
|
||||||
|
should_animate = true;
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should_animate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_animation_snapshot(&mut self, renderer: &mut GlesRenderer) {
|
||||||
|
self.animation_snapshot = Some(self.render_snapshot(renderer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Mapped {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
remove_pre_commit_hook(self.toplevel().wl_surface(), self.pre_commit_hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutElement for Mapped {
|
||||||
|
type Id = Window;
|
||||||
|
|
||||||
|
fn id(&self) -> &Self::Id {
|
||||||
|
&self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<i32, Logical> {
|
||||||
|
self.window.geometry().size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buf_loc(&self) -> Point<i32, Logical> {
|
||||||
|
Point::from((0, 0)) - self.window.geometry().loc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_in_input_region(&self, point: Point<f64, Logical>) -> bool {
|
||||||
|
let surface_local = point + self.window.geometry().loc.to_f64();
|
||||||
|
self.window.is_in_input_region(&surface_local)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<R: NiriRenderer>(
|
||||||
|
&self,
|
||||||
|
renderer: &mut R,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
alpha: f32,
|
||||||
|
target: RenderTarget,
|
||||||
|
) -> Vec<LayoutElementRenderElement<R>> {
|
||||||
|
let block_out = match self.rules.block_out_from {
|
||||||
|
None => false,
|
||||||
|
Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast,
|
||||||
|
Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output,
|
||||||
|
};
|
||||||
|
|
||||||
|
if block_out {
|
||||||
|
let mut buffer = self.block_out_buffer.borrow_mut();
|
||||||
|
buffer.resize(self.window.geometry().size);
|
||||||
|
let elem = SolidColorRenderElement::from_buffer(
|
||||||
|
&buffer,
|
||||||
|
location.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
vec![elem.into()]
|
||||||
|
} else {
|
||||||
|
let buf_pos = location - self.window.geometry().loc;
|
||||||
|
let buf_pos = buf_pos.to_physical_precise_round(scale);
|
||||||
|
self.window.render_elements(renderer, buf_pos, scale, alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_size(&mut self, size: Size<i32, Logical>, animate: bool) {
|
||||||
|
let changed = self.toplevel().with_pending_state(|state| {
|
||||||
|
let changed = state.size != Some(size);
|
||||||
|
state.size = Some(size);
|
||||||
|
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||||
|
changed
|
||||||
|
});
|
||||||
|
|
||||||
|
if changed && animate {
|
||||||
|
self.animate_next_configure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_fullscreen(&self, size: Size<i32, Logical>) {
|
||||||
|
self.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(size);
|
||||||
|
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = with_states(self.toplevel().wl_surface(), |state| {
|
||||||
|
let curr = state.cached_state.current::<SurfaceCachedState>();
|
||||||
|
curr.min_size
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(x) = self.rules.min_width {
|
||||||
|
size.w = max(size.w, i32::from(x));
|
||||||
|
}
|
||||||
|
if let Some(x) = self.rules.min_height {
|
||||||
|
size.h = max(size.h, i32::from(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_size(&self) -> Size<i32, Logical> {
|
||||||
|
let mut size = with_states(self.toplevel().wl_surface(), |state| {
|
||||||
|
let curr = state.cached_state.current::<SurfaceCachedState>();
|
||||||
|
curr.max_size
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(x) = self.rules.max_width {
|
||||||
|
if size.w == 0 {
|
||||||
|
size.w = i32::from(x);
|
||||||
|
} else if x > 0 {
|
||||||
|
size.w = min(size.w, i32::from(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(x) = self.rules.max_height {
|
||||||
|
if size.h == 0 {
|
||||||
|
size.h = i32::from(x);
|
||||||
|
} else if x > 0 {
|
||||||
|
size.h = min(size.h, i32::from(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_wl_surface(&self, wl_surface: &WlSurface) -> bool {
|
||||||
|
self.toplevel().wl_surface() == wl_surface
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_preferred_scale_transform(&self, scale: i32, transform: Transform) {
|
||||||
|
self.window.with_surfaces(|surface, data| {
|
||||||
|
send_surface_state(surface, data, scale, transform);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_ssd(&self) -> bool {
|
||||||
|
self.toplevel().current_state().decoration_mode
|
||||||
|
== Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_enter(&self, output: &Output) {
|
||||||
|
let overlap = Rectangle::from_loc_and_size((0, 0), (i32::MAX, i32::MAX));
|
||||||
|
self.window.output_enter(output, overlap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_leave(&self, output: &Output) {
|
||||||
|
self.window.output_leave(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_offscreen_element_id(&self, id: Option<Id>) {
|
||||||
|
let data = self
|
||||||
|
.window
|
||||||
|
.user_data()
|
||||||
|
.get_or_insert(WindowOffscreenId::default);
|
||||||
|
data.0.replace(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_activated(&mut self, active: bool) {
|
||||||
|
let changed = self.toplevel().with_pending_state(|state| {
|
||||||
|
if active {
|
||||||
|
state.states.set(xdg_toplevel::State::Activated)
|
||||||
|
} else {
|
||||||
|
state.states.unset(xdg_toplevel::State::Activated)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.need_to_recompute_rules |= changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_bounds(&self, bounds: Size<i32, Logical>) {
|
||||||
|
self.toplevel().with_pending_state(|state| {
|
||||||
|
state.bounds = Some(bounds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_pending_configure(&mut self) {
|
||||||
|
if let Some(serial) = self.toplevel().send_pending_configure() {
|
||||||
|
if self.animate_next_configure {
|
||||||
|
self.animate_serials.push(serial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.animate_next_configure = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_fullscreen(&self) -> bool {
|
||||||
|
self.toplevel()
|
||||||
|
.current_state()
|
||||||
|
.states
|
||||||
|
.contains(xdg_toplevel::State::Fullscreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pending_fullscreen(&self) -> bool {
|
||||||
|
self.toplevel()
|
||||||
|
.with_pending_state(|state| state.states.contains(xdg_toplevel::State::Fullscreen))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh(&self) {
|
||||||
|
self.window.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rules(&self) -> &ResolvedWindowRules {
|
||||||
|
&self.rules
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_unmap_snapshot(&self) -> Option<LayoutElementRenderSnapshot> {
|
||||||
|
self.unmap_snapshot.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animation_snapshot(&self) -> Option<&LayoutElementRenderSnapshot> {
|
||||||
|
self.animation_snapshot.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_animation_snapshot(&mut self) -> Option<LayoutElementRenderSnapshot> {
|
||||||
|
self.animation_snapshot.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
use niri_config::{BlockOutFrom, Match, WindowRule};
|
||||||
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||||
|
use smithay::wayland::compositor::with_states;
|
||||||
|
use smithay::wayland::shell::xdg::{
|
||||||
|
ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::layout::workspace::ColumnWidth;
|
||||||
|
|
||||||
|
pub mod mapped;
|
||||||
|
pub use mapped::Mapped;
|
||||||
|
|
||||||
|
pub mod unmapped;
|
||||||
|
pub use unmapped::{InitialConfigureState, Unmapped};
|
||||||
|
|
||||||
|
/// Reference to a mapped or unmapped window.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum WindowRef<'a> {
|
||||||
|
Unmapped(&'a Unmapped),
|
||||||
|
Mapped(&'a Mapped),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rules fully resolved for a window.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct ResolvedWindowRules {
|
||||||
|
/// Default width for this window.
|
||||||
|
///
|
||||||
|
/// - `None`: unset (global default should be used).
|
||||||
|
/// - `Some(None)`: set to empty (window picks its own width).
|
||||||
|
/// - `Some(Some(width))`: set to a particular width.
|
||||||
|
pub default_width: Option<Option<ColumnWidth>>,
|
||||||
|
|
||||||
|
/// Output to open this window on.
|
||||||
|
pub open_on_output: Option<String>,
|
||||||
|
|
||||||
|
/// Whether the window should open full-width.
|
||||||
|
pub open_maximized: Option<bool>,
|
||||||
|
|
||||||
|
/// Whether the window should open fullscreen.
|
||||||
|
pub open_fullscreen: Option<bool>,
|
||||||
|
|
||||||
|
/// Extra bound on the minimum window width.
|
||||||
|
pub min_width: Option<u16>,
|
||||||
|
/// Extra bound on the minimum window height.
|
||||||
|
pub min_height: Option<u16>,
|
||||||
|
/// Extra bound on the maximum window width.
|
||||||
|
pub max_width: Option<u16>,
|
||||||
|
/// Extra bound on the maximum window height.
|
||||||
|
pub max_height: Option<u16>,
|
||||||
|
|
||||||
|
/// Whether or not to draw the border with a solid background.
|
||||||
|
///
|
||||||
|
/// `None` means using the SSD heuristic.
|
||||||
|
pub draw_border_with_background: Option<bool>,
|
||||||
|
|
||||||
|
/// Extra opacity to draw this window with.
|
||||||
|
pub opacity: Option<f32>,
|
||||||
|
|
||||||
|
/// Whether to block out this window from certain render targets.
|
||||||
|
pub block_out_from: Option<BlockOutFrom>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WindowRef<'a> {
|
||||||
|
pub fn toplevel(self) -> &'a ToplevelSurface {
|
||||||
|
match self {
|
||||||
|
WindowRef::Unmapped(unmapped) => unmapped.toplevel(),
|
||||||
|
WindowRef::Mapped(mapped) => mapped.toplevel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_focused(self) -> bool {
|
||||||
|
match self {
|
||||||
|
WindowRef::Unmapped(_) => false,
|
||||||
|
WindowRef::Mapped(mapped) => mapped.is_focused(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedWindowRules {
|
||||||
|
pub const fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
default_width: None,
|
||||||
|
open_on_output: None,
|
||||||
|
open_maximized: None,
|
||||||
|
open_fullscreen: None,
|
||||||
|
min_width: None,
|
||||||
|
min_height: None,
|
||||||
|
max_width: None,
|
||||||
|
max_height: None,
|
||||||
|
draw_border_with_background: None,
|
||||||
|
opacity: None,
|
||||||
|
block_out_from: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute(rules: &[WindowRule], window: WindowRef) -> Self {
|
||||||
|
let _span = tracy_client::span!("ResolvedWindowRules::compute");
|
||||||
|
|
||||||
|
let mut resolved = ResolvedWindowRules::empty();
|
||||||
|
|
||||||
|
let toplevel = window.toplevel();
|
||||||
|
with_states(toplevel.wl_surface(), |states| {
|
||||||
|
let mut role = states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure server_pending like in Smithay's with_pending_state().
|
||||||
|
if role.server_pending.is_none() {
|
||||||
|
role.server_pending = Some(role.current_server_state().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open_on_output = None;
|
||||||
|
|
||||||
|
for rule in rules {
|
||||||
|
let matches = |m| window_matches(window, &role, m);
|
||||||
|
|
||||||
|
if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.excludes.iter().any(matches) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule
|
||||||
|
.default_column_width
|
||||||
|
.as_ref()
|
||||||
|
.map(|d| d.0.map(ColumnWidth::from))
|
||||||
|
{
|
||||||
|
resolved.default_width = Some(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule.open_on_output.as_deref() {
|
||||||
|
open_on_output = Some(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule.open_maximized {
|
||||||
|
resolved.open_maximized = Some(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule.open_fullscreen {
|
||||||
|
resolved.open_fullscreen = Some(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule.min_width {
|
||||||
|
resolved.min_width = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.min_height {
|
||||||
|
resolved.min_height = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.max_width {
|
||||||
|
resolved.max_width = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.max_height {
|
||||||
|
resolved.max_height = Some(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = rule.draw_border_with_background {
|
||||||
|
resolved.draw_border_with_background = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.opacity {
|
||||||
|
resolved.opacity = Some(x);
|
||||||
|
}
|
||||||
|
if let Some(x) = rule.block_out_from {
|
||||||
|
resolved.block_out_from = Some(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
|
||||||
|
});
|
||||||
|
|
||||||
|
resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool {
|
||||||
|
// Must be ensured by the caller.
|
||||||
|
let server_pending = role.server_pending.as_ref().unwrap();
|
||||||
|
|
||||||
|
if let Some(is_focused) = m.is_focused {
|
||||||
|
if window.is_focused() != is_focused {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(is_active) = m.is_active {
|
||||||
|
// Our "is-active" definition corresponds to the window having a pending Activated state.
|
||||||
|
let pending_activated = server_pending
|
||||||
|
.states
|
||||||
|
.contains(xdg_toplevel::State::Activated);
|
||||||
|
if is_active != pending_activated {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(app_id_re) = &m.app_id {
|
||||||
|
let Some(app_id) = &role.app_id else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !app_id_re.is_match(app_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(title_re) = &m.title {
|
||||||
|
let Some(title) = &role.title else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !title_re.is_match(title) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use smithay::desktop::Window;
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::wayland::shell::xdg::ToplevelSurface;
|
||||||
|
|
||||||
|
use super::ResolvedWindowRules;
|
||||||
|
use crate::layout::workspace::ColumnWidth;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Unmapped {
|
||||||
|
pub window: Window,
|
||||||
|
pub state: InitialConfigureState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InitialConfigureState {
|
||||||
|
/// The window has not been initially configured yet.
|
||||||
|
NotConfigured {
|
||||||
|
/// Whether the window requested to be fullscreened, and the requested output, if any.
|
||||||
|
wants_fullscreen: Option<Option<Output>>,
|
||||||
|
},
|
||||||
|
/// The window has been configured.
|
||||||
|
Configured {
|
||||||
|
/// Up-to-date rules.
|
||||||
|
///
|
||||||
|
/// We start tracking window rules when sending the initial configure, since they don't
|
||||||
|
/// affect anything before that.
|
||||||
|
rules: ResolvedWindowRules,
|
||||||
|
|
||||||
|
/// Resolved default width for this window.
|
||||||
|
///
|
||||||
|
/// `None` means that the window will pick its own width.
|
||||||
|
width: Option<ColumnWidth>,
|
||||||
|
|
||||||
|
/// Whether the window should open full-width.
|
||||||
|
is_full_width: bool,
|
||||||
|
|
||||||
|
/// Output to open this window on.
|
||||||
|
///
|
||||||
|
/// This can be `None` in cases like:
|
||||||
|
///
|
||||||
|
/// - There are no outputs connected.
|
||||||
|
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
|
||||||
|
/// should fetch the parent's current output again upon mapping.
|
||||||
|
output: Option<Output>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Unmapped {
|
||||||
|
/// Wraps a newly created window that hasn't been initially configured yet.
|
||||||
|
pub fn new(window: Window) -> Self {
|
||||||
|
Self {
|
||||||
|
window,
|
||||||
|
state: InitialConfigureState::NotConfigured {
|
||||||
|
wants_fullscreen: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_initial_configure(&self) -> bool {
|
||||||
|
matches!(self.state, InitialConfigureState::NotConfigured { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toplevel(&self) -> &ToplevelSurface {
|
||||||
|
self.window.toplevel().expect("no X11 support")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
### VSCode
|
||||||
|
|
||||||
|
There seems to be a bug in VSCode's Wayland backend until 1.86.0 which causes the window to not show up when using server-side decorations. So, to run VSCode:
|
||||||
|
|
||||||
|
1. Make sure VSCode is 1.86.0 or above, or that `prefer-no-csd` is **not set** in the niri config
|
||||||
|
2. Run `code --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations`
|
||||||
|
|
||||||
|
Also, if you're having issues with some VSCode hotkeys, try starting `Xwayland` and setting the `DISPLAY=:0` environment variable for VSCode. That is, still running VSCode with the Wayland backend, but with `DISPLAY` set to a running Xwayland instance. Apparently, VSCode currently unconditionally queries the X server for a keymap.
|
||||||
|
|
||||||
|
### Chromium
|
||||||
|
|
||||||
|
When creating new windows within Chromium (e.g. with <kbd>Ctrl</kbd><kbd>N</kbd>), there's a Chromium bug with sizing:
|
||||||
|
|
||||||
|
- With CSD (`prefer-no-csd` unset), the window will be a bit smaller than needed
|
||||||
|
- With SSD (`prefer-no-csd` set), the window buffer will be offset to the top-left
|
||||||
|
|
||||||
|
Both of these can be fixed by resizing the new Chromium window.
|
||||||
|
|
||||||
|
### WezTerm
|
||||||
|
|
||||||
|
There's [a bug](https://github.com/wez/wezterm/issues/4708) in WezTerm that it waits for a zero-sized Wayland configure event, so its window never shows up in niri. To work around it, put this window rule in the niri config (included in the default config):
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||||
|
default-column-width {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This empty default column width lets WezTerm pick its own initial width which makes it show up properly.
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
Niri has several animations which you can configure in the same way.
|
||||||
|
Additionally, you can disable or slow down all animations at once.
|
||||||
|
|
||||||
|
Here's a quick glance at the available animations with their default values.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
// Uncomment to turn off all animations.
|
||||||
|
// You can also put "off" into each individual animation to disable it.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// Slow down all animations by this factor. Values below 1 speed them up instead.
|
||||||
|
// slowdown 3.0
|
||||||
|
|
||||||
|
// Individual animations.
|
||||||
|
|
||||||
|
workspace-switch {
|
||||||
|
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||||
|
}
|
||||||
|
|
||||||
|
window-open {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-expo"
|
||||||
|
}
|
||||||
|
|
||||||
|
window-close {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-quad"
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontal-view-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
|
||||||
|
window-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
|
||||||
|
window-resize {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
|
||||||
|
config-notification-open-close {
|
||||||
|
spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animation Types
|
||||||
|
|
||||||
|
There are two animation types: easing and spring.
|
||||||
|
Each animation can be either an easing or a spring.
|
||||||
|
|
||||||
|
#### Easing
|
||||||
|
|
||||||
|
This is a relatively common animation type that changes the value over a set duration using an interpolation curve.
|
||||||
|
|
||||||
|
To use this animation, set the following parameters:
|
||||||
|
|
||||||
|
- `duration-ms`: duration of the animation in milliseconds.
|
||||||
|
- `curve`: the easing curve to use.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
window-open {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-expo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently, niri only supports three curves:
|
||||||
|
|
||||||
|
- `ease-out-quad` <sup>Since: 0.1.5</sup>
|
||||||
|
- `ease-out-cubic`
|
||||||
|
- `ease-out-expo`
|
||||||
|
|
||||||
|
You can get a feel for them on pages like [easings.net](https://easings.net/).
|
||||||
|
|
||||||
|
#### Spring
|
||||||
|
|
||||||
|
Spring animations use a model of a physical spring to animate the value.
|
||||||
|
They notably feel better with touchpad gestures, because they take into account the velocity of your fingers as you release the swipe.
|
||||||
|
Springs can also oscillate / bounce at the end with the right parameters if you like that sort of thing, but they don't have to (and by default they mostly don't).
|
||||||
|
|
||||||
|
Due to springs using a physical model, the animation parameters are less obvious and generally should be tuned with trial and error.
|
||||||
|
Notably, you cannot directly set the duration.
|
||||||
|
You can use the [Elastic](https://flathub.org/apps/app.drey.Elastic) app to help visualize how the spring parameters change the animation.
|
||||||
|
|
||||||
|
A spring animation is configured like this, with three mandatory parameters:
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
workspace-switch {
|
||||||
|
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `damping-ratio` goes from 0.1 to 10.0 and has the following properties:
|
||||||
|
|
||||||
|
- below 1.0: underdamped spring, will oscillate in the end.
|
||||||
|
- above 1.0: overdamped spring, won't oscillate.
|
||||||
|
- 1.0: critically damped spring, comes to rest in minimum possible time without oscillations.
|
||||||
|
|
||||||
|
However, even with damping ratio = 1.0, the spring animation may oscillate if "launched" with enough velocity from a touchpad swipe.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Overdamped springs currently have some numerical stability issues and may cause graphical glitches.
|
||||||
|
> Therefore, setting `damping-ratio` above `1.0` is not recommended.
|
||||||
|
|
||||||
|
Lower `stiffness` will result in a slower animation more prone to oscillation.
|
||||||
|
|
||||||
|
Set `epsilon` to a lower value if the animation "jumps" at the end.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> The spring *mass* (which you can see in Elastic) is hardcoded to 1.0 and cannot be changed.
|
||||||
|
> Instead, change `stiffness` proportionally.
|
||||||
|
> E.g. increasing mass by 2× is the same as decreasing stiffness by 2×.
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
|
||||||
|
Now let's go into more detail on the animations that you can configure.
|
||||||
|
|
||||||
|
#### `workspace-switch`
|
||||||
|
|
||||||
|
Animation when switching workspaces up and down, including after the vertical touchpad gesture (a spring is recommended).
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
workspace-switch {
|
||||||
|
spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `window-open`
|
||||||
|
|
||||||
|
Window opening animation.
|
||||||
|
|
||||||
|
This one uses an easing type by default.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
window-open {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-expo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `window-close`
|
||||||
|
|
||||||
|
<sup>Since: 0.1.5</sup>
|
||||||
|
|
||||||
|
Window closing animation.
|
||||||
|
|
||||||
|
This one uses an easing type by default.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
window-open {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-quad"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `horizontal-view-movement`
|
||||||
|
|
||||||
|
All horizontal camera view movement animations, such as:
|
||||||
|
|
||||||
|
- When a window off-screen is focused and the camera scrolls to it.
|
||||||
|
- When a new window appears off-screen and the camera scrolls to it.
|
||||||
|
- After a horizontal touchpad gesture (a spring is recommended).
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
horizontal-view-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `window-movement`
|
||||||
|
|
||||||
|
<sup>Since: 0.1.5</sup>
|
||||||
|
|
||||||
|
Movement of individual windows within a workspace.
|
||||||
|
|
||||||
|
Includes:
|
||||||
|
|
||||||
|
- Moving window columns with `move-column-left` and `move-column-right`.
|
||||||
|
- Moving windows inside a column with `move-window-up` and `move-window-down`.
|
||||||
|
- Moving windows out of the way upon window opening and closing.
|
||||||
|
- Window movement between columns when consuming/expelling.
|
||||||
|
|
||||||
|
This animation *does not* include the camera view movement, such as scrolling the workspace left and right.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
window-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `window-resize`
|
||||||
|
|
||||||
|
<sup>Since: 0.1.5</sup>
|
||||||
|
|
||||||
|
Window resize animation.
|
||||||
|
|
||||||
|
Only manual window resizes are animated, i.e. when you resize the window with `switch-preset-column-width` or `maximize-column`.
|
||||||
|
Also, very small resizes (up to 10 pixels) are not animated.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
window-resize {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `config-notification-open-close`
|
||||||
|
|
||||||
|
The open/close animation of the config parse error and new default config notifications.
|
||||||
|
|
||||||
|
This one uses an underdamped spring by default (`damping-ratio=0.6`) which causes a slight oscillation in the end.
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
config-notification-open-close {
|
||||||
|
spring damping-ratio=0.6 stiffness=1000 epsilon=0.001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronized Animations
|
||||||
|
|
||||||
|
<sup>Since: 0.1.5</sup>
|
||||||
|
|
||||||
|
Sometimes, when two animations are meant to play together synchronized, niri will drive them both with the same configuration.
|
||||||
|
|
||||||
|
For example, if a window resize causes the view to move, then that view movement animation will also use the `window-resize` configuration (rather than the `horizontal-view-movement` configuration).
|
||||||
|
This is especially important for animated resizes to look good when using `center-focused-column "always"`.
|
||||||
|
|
||||||
|
As another example, resizing a window in a column vertically causes other windows to move up or down into their new position.
|
||||||
|
This movement will use the `window-resize` configuration, rather than the `window-movement` configuration, to keep the animations synchronized.
|
||||||
|
|
||||||
|
A few actions are still missing this synchronization logic, since in some cases it is difficult to implement properly.
|
||||||
|
Therefore, for the best results, consider using the same parameters for related animations (they are all the same by default):
|
||||||
|
|
||||||
|
- `horizontal-view-movement`
|
||||||
|
- `window-movement`
|
||||||
|
- `window-resize`
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
Niri has several options that are only useful for debugging, or are experimental and have known issues.
|
||||||
|
They are not meant for normal use.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> These options are **not** covered by the [config breaking change policy](./Configuration:-Overview.md).
|
||||||
|
> They can change or stop working at any point with little notice.
|
||||||
|
|
||||||
|
Here are all the options at a glance:
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
preview-render "screencast"
|
||||||
|
// preview-render "screen-capture"
|
||||||
|
enable-overlay-planes
|
||||||
|
disable-cursor-plane
|
||||||
|
render-drm-device "/dev/dri/renderD129"
|
||||||
|
dbus-interfaces-in-non-session-instances
|
||||||
|
wait-for-frame-completion-before-queueing
|
||||||
|
emulate-zero-presentation-time
|
||||||
|
enable-color-transformations-capability
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `preview-render`
|
||||||
|
|
||||||
|
Make niri render the monitors the same way as for a screencast or a screen capture.
|
||||||
|
|
||||||
|
Useful for previewing the `block-out-from` window rule.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
preview-render "screencast"
|
||||||
|
// preview-render "screen-capture"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `enable-overlay-planes`
|
||||||
|
|
||||||
|
Enable direct scanout into overlay planes.
|
||||||
|
May cause frame drops during some animations on some hardware (which is why it is not the default).
|
||||||
|
|
||||||
|
Direct scanout into the primary plane is always enabled.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
enable-overlay-planes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `disable-cursor-plane`
|
||||||
|
|
||||||
|
Disable the use of the cursor plane.
|
||||||
|
The cursor will be rendered together with the rest of the frame.
|
||||||
|
|
||||||
|
Useful to work around driver bugs on specific hardware.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
disable-cursor-plane
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `render-drm-device`
|
||||||
|
|
||||||
|
Override the DRM device that niri will use for all rendering.
|
||||||
|
|
||||||
|
You can set this to make niri use a different primary GPU than the default one.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
render-drm-device "/dev/dri/renderD129"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `dbus-interfaces-in-non-session-instances`
|
||||||
|
|
||||||
|
Make niri create its D-Bus interfaces even if it's not running as a `--session`.
|
||||||
|
|
||||||
|
Useful for testing screencasting changes without having to relogin.
|
||||||
|
|
||||||
|
The main niri instance will *not* currently take back the interfaces when you close the test instance, so you will need to relogin in the end to make screencasting work again.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
dbus-interfaces-in-non-session-instances
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `wait-for-frame-completion-before-queueing`
|
||||||
|
|
||||||
|
Wait until every frame is done rendering before handing it over to DRM.
|
||||||
|
|
||||||
|
Useful for diagnosing certain synchronization and performance problems.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
wait-for-frame-completion-before-queueing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `emulate-zero-presentation-time`
|
||||||
|
|
||||||
|
Emulate zero (unknown) presentation time returned from DRM.
|
||||||
|
|
||||||
|
This is a thing on NVIDIA proprietary drivers, so this flag can be used to test that niri doesn't break too hard on those systems.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
emulate-zero-presentation-time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `enable-color-transformations-capability`
|
||||||
|
|
||||||
|
Enable the color-transformations capability of the Smithay renderer.
|
||||||
|
May cause a slight decrease in rendering performance.
|
||||||
|
|
||||||
|
Currently, should cause no visible changes in behavior, but it will be needed for HDR support whenever that happens.
|
||||||
|
So, this flag exists to be able to make sure that nothing breaks.
|
||||||
|
|
||||||
|
```
|
||||||
|
debug {
|
||||||
|
enable-color-transformations-capability
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `toggle-debug-tint` Key Binding
|
||||||
|
|
||||||
|
This one is not a debug option, but rather a key binding.
|
||||||
|
|
||||||
|
It will tint all surfaces green, unless they are being directly scanned out.
|
||||||
|
It's therefore useful to check if direct scanout is working.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
In this section you can configure input devices like keyboard and mouse, and some input-related options.
|
||||||
|
|
||||||
|
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
|
||||||
|
Settings in those sections will apply to every device of that type.
|
||||||
|
Currently, there's no way to configure specific devices individually (but that is planned).
|
||||||
|
|
||||||
|
All settings at a glance:
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
// layout "us"
|
||||||
|
// variant "colemak_dh_ortho"
|
||||||
|
// options "compose:ralt,ctrl:nocaps"
|
||||||
|
// model ""
|
||||||
|
// rules ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// repeat-delay 600
|
||||||
|
// repeat-rate 25
|
||||||
|
// track-layout "global"
|
||||||
|
}
|
||||||
|
|
||||||
|
touchpad {
|
||||||
|
tap
|
||||||
|
// dwt
|
||||||
|
// dwtp
|
||||||
|
natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// tap-button-map "left-middle-right"
|
||||||
|
// click-method "clickfinger"
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse {
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
}
|
||||||
|
|
||||||
|
trackpoint {
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
}
|
||||||
|
|
||||||
|
tablet {
|
||||||
|
map-to-output "eDP-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
touch {
|
||||||
|
map-to-output "eDP-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable-power-key-handling
|
||||||
|
// warp-mouse-to-focus
|
||||||
|
// focus-follows-mouse
|
||||||
|
// workspace-auto-back-and-forth
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard
|
||||||
|
|
||||||
|
#### Layout
|
||||||
|
|
||||||
|
In the `xkb` section, you can set layout, variant, options, model and rules.
|
||||||
|
These are passed directly to libxkbcommon, which is also used by most other Wayland compositors.
|
||||||
|
See the `xkeyboard-config(7)` manual for more information.
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
layout "us"
|
||||||
|
variant "colemak_dh_ortho"
|
||||||
|
options "compose:ralt,ctrl:nocaps"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When using multiple layouts, niri can remember the current layout globally (the default) or per-window.
|
||||||
|
You can control this with the `track-layout` option.
|
||||||
|
|
||||||
|
- `global`: layout change is global for all windows.
|
||||||
|
- `window`: layout is tracked for each window individually.
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
track-layout "global"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Repeat
|
||||||
|
|
||||||
|
Delay is in milliseconds before the keyboard repeat starts.
|
||||||
|
Rate is in characters per second.
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
repeat-delay 600
|
||||||
|
repeat-rate 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pointing Devices
|
||||||
|
|
||||||
|
Most settings for the pointing devices are passed directly to libinput.
|
||||||
|
Other Wayland compositors also use libinput, so it's likely you will find the same settings there.
|
||||||
|
For flags like `tap`, omit them or comment them out to disable the setting.
|
||||||
|
|
||||||
|
A few settings are common between `touchpad`, `mouse` and `trackpoint`:
|
||||||
|
|
||||||
|
- `natural-scroll`: if set, inverts the scrolling direction.
|
||||||
|
- `accel-speed`: pointer acceleration speed, valid values are from `-1.0` to `1.0` where the default is `0.0`.
|
||||||
|
- `accel-profile`: can be `adaptive` (the default) or `flat` (disables pointer acceleration).
|
||||||
|
|
||||||
|
Settings specific to `touchpad`s:
|
||||||
|
|
||||||
|
- `tap`: tap-to-click.
|
||||||
|
- `dwt`: disable-when-typing.
|
||||||
|
- `dwtp`: disable-when-trackpointing.
|
||||||
|
- `tap-button-map`: can be `left-right-middle` or `left-middle-right`, controls which button corresponds to a two-finger tap and a three-finger tap.
|
||||||
|
- `click-method`: can be `button-areas` or `clickfinger`, changes the [click method](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html).
|
||||||
|
|
||||||
|
Tablets and touchscreens are absolute pointing devices that can be mapped to a specific output like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
tablet {
|
||||||
|
map-to-output "eDP-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
touch {
|
||||||
|
map-to-output "eDP-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid output names are the same as the ones used for output configuration.
|
||||||
|
|
||||||
|
### General Settings
|
||||||
|
|
||||||
|
These settings are not specific to a particular input device.
|
||||||
|
|
||||||
|
#### `disable-power-key-handling`
|
||||||
|
|
||||||
|
By default, niri will take over the power button to make it sleep instead of power off.
|
||||||
|
Set this if you would like to configure the power button elsewhere (i.e. `logind.conf`).
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
disable-power-key-handling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `warp-mouse-to-focus`
|
||||||
|
|
||||||
|
Makes the mouse warp to newly focused windows.
|
||||||
|
|
||||||
|
X and Y coordinates are computed separately, i.e. if moving the mouse only horizontally is enough to put it inside the newly focused window, then it will move only horizontally.
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
warp-mouse-to-focus
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `focus-follows-mouse`
|
||||||
|
|
||||||
|
Focuses windows and outputs automatically when moving the mouse over them.
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
focus-follows-mouse
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `workspace-auto-back-and-forth`
|
||||||
|
|
||||||
|
Normally, switching to the same workspace by index twice will do nothing (since you're already on that workspace).
|
||||||
|
If this flag is enabled, switching to the same workspace by index twice will switch back to the previous workspace.
|
||||||
|
|
||||||
|
Niri will correctly switch to the workspace you came from, even if workspaces were reordered in the meantime.
|
||||||
|
|
||||||
|
```
|
||||||
|
input {
|
||||||
|
workspace-auto-back-and-forth
|
||||||
|
}
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user