mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
358 Commits
v0.1.0-beta.1
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
+869
-415
File diff suppressed because it is too large
Load Diff
+41
-26
@@ -1,5 +1,8 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["niri-visual-tests"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0-beta.1"
|
version = "0.1.4"
|
||||||
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,13 @@ 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"] }
|
||||||
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 +40,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.4", path = "niri-config" }
|
||||||
niri-config = { version = "0.1.0-beta.1", path = "niri-config" }
|
niri-ipc = { version = "0.1.4", 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 = "1.0.115"
|
||||||
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 +88,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 +97,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 +122,7 @@ lto = "thin"
|
|||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
version = "0.1.0~beta.1"
|
version = "0.1.4"
|
||||||
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 +133,4 @@ assets = [
|
|||||||
]
|
]
|
||||||
[package.metadata.generate-rpm.requires]
|
[package.metadata.generate-rpm.requires]
|
||||||
alacritty = "*"
|
alacritty = "*"
|
||||||
|
fuzzel = "*"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<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>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -16,26 +16,34 @@ 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
|
||||||
- Live-reloading config
|
- Live-reloading config
|
||||||
|
|
||||||
|
## Video Demo
|
||||||
|
|
||||||
|
https://github.com/YaLTeR/niri/assets/1794388/5d355694-7b06-4f00-8920-8dce54a8721c
|
||||||
|
|
||||||
## 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.
|
||||||
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 might have rendering issues.
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
@@ -44,25 +52,25 @@ 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.
|
## Packages
|
||||||
When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor.
|
|
||||||
|
There are several community-maintained distribution packages that you can use to install niri.
|
||||||
|
Here are some of them:
|
||||||
|
|
||||||
|
- Fedora COPR (I maintain this one myself): https://copr.fedorainfracloud.org/coprs/yalter/niri/
|
||||||
|
- AUR: [niri](https://aur.archlinux.org/packages/niri), [niri-bin](https://aur.archlinux.org/packages/niri-bin), [niri-git](https://aur.archlinux.org/packages/niri-git)
|
||||||
|
- NixOS Flake: https://github.com/sodiboo/niri-flake
|
||||||
|
- FreeBSD Ports: https://www.freshports.org/x11-wm/niri
|
||||||
|
- Gentoo GURU: https://gpo.zugaina.org/Overlays/guru/gui-wm/niri
|
||||||
|
|
||||||
## Building
|
## 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.
|
First, install the dependencies for your distribution.
|
||||||
|
|
||||||
- Ubuntu:
|
- Ubuntu 23.10:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt-get install -y software-properties-common
|
sudo apt-get install -y 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 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:
|
- Fedora:
|
||||||
@@ -71,12 +79,14 @@ First, install the dependencies for your distribution.
|
|||||||
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
|
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`.
|
Next, get latest stable Rust: https://rustup.rs/
|
||||||
|
|
||||||
|
Then, build niri with `cargo build --release`.
|
||||||
|
|
||||||
### NixOS/Nix
|
### 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`.
|
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:
|
If you're not on NixOS, you may need [NixGL](https://github.com/nix-community/nixGL) to run the resulting binary:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -122,20 +132,10 @@ 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.
|
[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
|
## Configuration
|
||||||
|
|
||||||
See [the wiki page](https://github.com/YaLTeR/niri/wiki/Xwayland) to learn how to use Xwayland with niri.
|
Please check [this wiki page](https://github.com/YaLTeR/niri/wiki/Configuration:-Overview) for an overview of niri configuration.
|
||||||
|
It also links to wiki pages containing thorough documentation for all options with examples.
|
||||||
### 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
|
## Default Hotkeys
|
||||||
|
|
||||||
@@ -184,17 +184,8 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
|
|||||||
| <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>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>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>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 |
|
| <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
|
||||||
@@ -202,4 +193,6 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
|||||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||||
[mako]: https://github.com/emersion/mako
|
[mako]: https://github.com/emersion/mako
|
||||||
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
[OBS]: https://flathub.org/apps/com.obsproject.Studio
|
||||||
|
[waybar]: https://github.com/Alexays/Waybar
|
||||||
|
[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.4", 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
|
||||||
|
|||||||
+1247
-146
File diff suppressed because it is too large
Load Diff
@@ -8,4 +8,8 @@ edition.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
clap = ["dep:clap"]
|
||||||
|
|||||||
+343
-3
@@ -2,6 +2,7 @@
|
|||||||
#![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};
|
||||||
|
|
||||||
@@ -9,19 +10,229 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||||
|
|
||||||
/// 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 information about connected outputs.
|
/// Request information about connected outputs.
|
||||||
Outputs,
|
Outputs,
|
||||||
|
/// Request information about the focused window.
|
||||||
|
FocusedWindow,
|
||||||
|
/// Perform an action.
|
||||||
|
Action(Action),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,
|
||||||
/// 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 +252,10 @@ 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>,
|
||||||
|
/// 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 +267,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,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.4", path = ".." }
|
||||||
|
niri-config = { version = "0.1.4", 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,231 @@
|
|||||||
|
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, window: TestWindow, width: Option<ColumnWidth>) {
|
||||||
|
self.layout.add_window(window.clone(), width, false);
|
||||||
|
if window.communicate() {
|
||||||
|
self.layout.update_window(window.id());
|
||||||
|
}
|
||||||
|
self.windows.push(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_window_right_of(
|
||||||
|
&mut self,
|
||||||
|
right_of: &TestWindow,
|
||||||
|
window: TestWindow,
|
||||||
|
width: Option<ColumnWidth>,
|
||||||
|
) {
|
||||||
|
self.layout
|
||||||
|
.add_window_right_of(right_of.id(), window.clone(), width, false);
|
||||||
|
if window.communicate() {
|
||||||
|
self.layout.update_window(window.id());
|
||||||
|
}
|
||||||
|
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,119 @@
|
|||||||
|
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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
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)));
|
||||||
|
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 window = TestWindow::freeform(0);
|
||||||
|
window.request_size(size);
|
||||||
|
window.communicate();
|
||||||
|
Self { window }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||||
|
let window = TestWindow::fixed_size(0);
|
||||||
|
window.request_size(size);
|
||||||
|
window.communicate();
|
||||||
|
Self { window }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||||
|
let window = TestWindow::fixed_size(0);
|
||||||
|
window.set_csd_shadow_width(64);
|
||||||
|
window.request_size(size);
|
||||||
|
window.communicate();
|
||||||
|
Self { window }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase for Window {
|
||||||
|
fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
self.window.request_size(Size::from((width, height)));
|
||||||
|
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,250 @@
|
|||||||
|
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::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")?;
|
||||||
|
|
||||||
|
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,229 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::cmp::{max, min};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use niri::layout::{LayoutElement, LayoutElementRenderElement};
|
||||||
|
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(&self, size: Size<i32, Logical>) {
|
||||||
|
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(&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
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
-106
@@ -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,7 +257,7 @@ 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"; }
|
||||||
@@ -263,6 +320,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 +342,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 +404,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 +446,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,282 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use keyframe::functions::EaseOutCubic;
|
||||||
|
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,
|
||||||
|
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 {
|
||||||
|
EaseOutCubic,
|
||||||
|
EaseOutExpo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation {
|
||||||
|
pub fn new(
|
||||||
|
from: f64,
|
||||||
|
to: f64,
|
||||||
|
initial_velocity: f64,
|
||||||
|
config: niri_config::Animation,
|
||||||
|
default: niri_config::Animation,
|
||||||
|
) -> Self {
|
||||||
|
if config.off {
|
||||||
|
return Self::ease(from, to, 0, Curve::EaseOutCubic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve defaults.
|
||||||
|
let (kind, easing_defaults) = match (config.kind, default.kind) {
|
||||||
|
// Configured spring.
|
||||||
|
(configured @ niri_config::AnimationKind::Spring(_), _) => (configured, None),
|
||||||
|
// Configured nothing, defaults spring.
|
||||||
|
(
|
||||||
|
niri_config::AnimationKind::Easing(easing),
|
||||||
|
defaults @ niri_config::AnimationKind::Spring(_),
|
||||||
|
) if easing == niri_config::EasingParams::unfilled() => (defaults, None),
|
||||||
|
// Configured easing or nothing, defaults easing.
|
||||||
|
(
|
||||||
|
configured @ niri_config::AnimationKind::Easing(_),
|
||||||
|
niri_config::AnimationKind::Easing(defaults),
|
||||||
|
) => (configured, Some(defaults)),
|
||||||
|
// Configured easing, defaults spring.
|
||||||
|
(
|
||||||
|
configured @ niri_config::AnimationKind::Easing(_),
|
||||||
|
niri_config::AnimationKind::Spring(_),
|
||||||
|
) => (configured, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
niri_config::AnimationKind::Spring(p) => {
|
||||||
|
let params = SpringParams::new(p.damping_ratio, f64::from(p.stiffness), p.epsilon);
|
||||||
|
|
||||||
|
let spring = Spring {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
initial_velocity,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
Self::spring(spring)
|
||||||
|
}
|
||||||
|
niri_config::AnimationKind::Easing(p) => {
|
||||||
|
let defaults = easing_defaults.unwrap_or(niri_config::EasingParams::default());
|
||||||
|
let duration_ms = p.duration_ms.or(defaults.duration_ms).unwrap();
|
||||||
|
let curve = Curve::from(p.curve.or(defaults.curve).unwrap());
|
||||||
|
Self::ease(from, to, u64::from(duration_ms), curve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ease(from: f64, to: 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,
|
||||||
|
duration,
|
||||||
|
start_time: now,
|
||||||
|
current_time: now,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spring(spring: Spring) -> 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 = spring.duration();
|
||||||
|
let kind = Kind::Spring(spring);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
from: spring.from,
|
||||||
|
to: spring.to,
|
||||||
|
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,
|
||||||
|
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 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) => spring.value_at(passed),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to(&self) -> f64 {
|
||||||
|
self.to
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn from(&self) -> f64 {
|
||||||
|
self.from
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Curve {
|
||||||
|
pub fn y(self, x: f64) -> f64 {
|
||||||
|
match self {
|
||||||
|
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::EaseOutCubic => Curve::EaseOutCubic,
|
||||||
|
niri_config::AnimationCurve::EaseOutExpo => Curve::EaseOutExpo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(_) => (),
|
||||||
|
|||||||
+758
-216
File diff suppressed because it is too large
Load Diff
+50
-46
@@ -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::{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,13 @@ 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),
|
||||||
|
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,44 +93,45 @@ 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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shaders::init(renderer);
|
||||||
|
|
||||||
niri.add_output(self.output.clone(), None);
|
niri.add_output(self.output.clone(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +150,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();
|
||||||
@@ -195,12 +199,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 +221,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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
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 {
|
||||||
|
/// List connected outputs.
|
||||||
|
Outputs,
|
||||||
|
/// Print information about the focused window.
|
||||||
|
FocusedWindow,
|
||||||
|
/// Perform an action.
|
||||||
|
Action {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: Action,
|
||||||
|
},
|
||||||
|
}
|
||||||
+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,
|
||||||
};
|
};
|
||||||
|
|||||||
+97
-20
@@ -16,9 +16,8 @@ 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 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 +74,7 @@ impl CompositorHandler for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&mut self, surface: &WlSurface) {
|
fn commit(&mut self, surface: &WlSurface) {
|
||||||
@@ -97,41 +96,117 @@ impl CompositorHandler for State {
|
|||||||
// 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 (rules, width, is_full_width, output) =
|
||||||
{
|
if let InitialConfigureState::Configured {
|
||||||
self.niri.queue_redraw(output);
|
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 = window
|
||||||
|
.toplevel()
|
||||||
|
.expect("no x11 support")
|
||||||
|
.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 mapped = Mapped::new(window, rules);
|
||||||
|
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();
|
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
|
||||||
|
});
|
||||||
|
|
||||||
if !is_mapped {
|
if !is_mapped {
|
||||||
// The toplevel got unmapped.
|
// The toplevel got unmapped.
|
||||||
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);
|
// 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 +216,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 +225,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 +238,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,7 +263,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+208
-14
@@ -9,10 +9,12 @@ 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::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 +22,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,
|
||||||
@@ -39,18 +47,26 @@ use smithay::wayland::session_lock::{
|
|||||||
};
|
};
|
||||||
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,6 +89,19 @@ 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);
|
||||||
@@ -115,7 +144,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 +218,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 +238,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 +299,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 +308,166 @@ 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 {
|
||||||
|
&mut self
|
||||||
|
.backend
|
||||||
|
.tty()
|
||||||
|
.get_device_from_node(node)
|
||||||
|
.unwrap()
|
||||||
|
.drm_lease_state
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|||||||
+361
-69
@@ -13,6 +13,7 @@ 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::{send_surface_state, with_states};
|
||||||
|
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 +21,14 @@ 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::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 +37,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 +80,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 +180,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 +193,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 +373,40 @@ 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();
|
||||||
|
|
||||||
|
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 +459,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 +481,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 +656,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 +713,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 +729,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(
|
||||||
|
|||||||
+949
-140
File diff suppressed because it is too large
Load Diff
+93
-11
@@ -3,10 +3,10 @@ use std::io::{Read, Write};
|
|||||||
use std::net::Shutdown;
|
use std::net::Shutdown;
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use niri_ipc::{Mode, Output, Request, Response};
|
use niri_ipc::{LogicalOutput, Mode, Output, Reply, Request, Response};
|
||||||
|
|
||||||
use crate::Msg;
|
use crate::cli::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 socket_path = env::var_os(niri_ipc::SOCKET_PATH_ENV).with_context(|| {
|
||||||
@@ -19,8 +19,10 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
let mut stream =
|
let mut stream =
|
||||||
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?;
|
||||||
|
|
||||||
let request = match msg {
|
let request = match &msg {
|
||||||
Msg::Outputs => Request::Outputs,
|
Msg::Outputs => Request::Outputs,
|
||||||
|
Msg::FocusedWindow => Request::FocusedWindow,
|
||||||
|
Msg::Action { action } => Request::Action(action.clone()),
|
||||||
};
|
};
|
||||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||||
stream
|
stream
|
||||||
@@ -35,12 +37,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
.read_to_end(&mut buf)
|
.read_to_end(&mut buf)
|
||||||
.context("error reading IPC response")?;
|
.context("error reading IPC response")?;
|
||||||
|
|
||||||
let response = serde_json::from_slice(&buf).context("error parsing IPC response")?;
|
let reply: Reply = serde_json::from_slice(&buf).context("error parsing IPC reply")?;
|
||||||
|
|
||||||
|
let response = reply
|
||||||
|
.map_err(|msg| anyhow!(msg))
|
||||||
|
.context("niri could not handle the request")?;
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
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 +67,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
physical_size,
|
physical_size,
|
||||||
modes,
|
modes,
|
||||||
current_mode,
|
current_mode,
|
||||||
|
logical,
|
||||||
} = output;
|
} = output;
|
||||||
|
|
||||||
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
||||||
@@ -74,9 +80,11 @@ 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");
|
||||||
}
|
}
|
||||||
@@ -87,19 +95,93 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
|||||||
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 {
|
||||||
|
niri_ipc::Transform::Normal => "normal",
|
||||||
|
niri_ipc::Transform::_90 => "90° counter-clockwise",
|
||||||
|
niri_ipc::Transform::_180 => "180°",
|
||||||
|
niri_ipc::Transform::_270 => "270° counter-clockwise",
|
||||||
|
niri_ipc::Transform::Flipped => "flipped horizontally",
|
||||||
|
niri_ipc::Transform::Flipped90 => {
|
||||||
|
"90° counter-clockwise, flipped horizontally"
|
||||||
|
}
|
||||||
|
niri_ipc::Transform::Flipped180 => "flipped vertically",
|
||||||
|
niri_ipc::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(())
|
||||||
|
|||||||
+56
-17
@@ -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;
|
||||||
@@ -11,10 +9,14 @@ 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::{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;
|
||||||
|
|
||||||
pub struct IpcServer {
|
pub struct IpcServer {
|
||||||
@@ -22,7 +24,9 @@ pub struct IpcServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +89,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 +114,53 @@ 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 reply = process(&ctx, &buf).map_err(|err| {
|
||||||
|
warn!("error processing IPC request: {err:?}");
|
||||||
|
err.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
let response = match request {
|
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||||
Request::Outputs => {
|
write.write_all(&buf).await.context("error writing reply")?;
|
||||||
let ipc_outputs = ctx.ipc_outputs.borrow().clone();
|
|
||||||
Response::Outputs(ipc_outputs)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let buf = serde_json::to_vec(&response).context("error formatting response")?;
|
|
||||||
write
|
|
||||||
.write_all(&buf)
|
|
||||||
.await
|
|
||||||
.context("error writing response")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process(ctx: &ClientCtx, buf: &str) -> anyhow::Result<Response> {
|
||||||
|
let request: Request = serde_json::from_str(buf).context("error parsing request")?;
|
||||||
|
|
||||||
|
let response = match request {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
Response::Handled
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+784
-228
File diff suppressed because it is too large
Load Diff
+261
-35
@@ -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,23 @@ 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,
|
||||||
|
niri_config::Animation::default_workspace_switch(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +158,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 +267,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();
|
||||||
}
|
}
|
||||||
@@ -409,6 +468,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 +481,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();
|
||||||
}
|
}
|
||||||
@@ -512,8 +594,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 +616,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 +652,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 +697,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 +713,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 +798,88 @@ 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,
|
||||||
|
niri_config::Animation::default_workspace_switch(),
|
||||||
|
)));
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+187
-45
@@ -3,14 +3,17 @@ use std::rc::Rc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
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::utils::{Logical, Point, Rectangle, Scale, Size};
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
|
||||||
|
|
||||||
use super::focus_ring::FocusRing;
|
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||||
use super::workspace::WorkspaceRenderElement;
|
use super::{LayoutElement, LayoutElementRenderElement, Options};
|
||||||
use super::{LayoutElement, Options};
|
use crate::animation::Animation;
|
||||||
|
use crate::niri_render_elements;
|
||||||
|
use crate::render_helpers::offscreen::OffscreenRenderElement;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::RenderTarget;
|
||||||
|
|
||||||
/// Toplevel window with decorations.
|
/// Toplevel window with decorations.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -21,6 +24,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 +42,39 @@ 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>,
|
||||||
|
|
||||||
/// Configurable properties of the layout.
|
/// Configurable properties of the layout.
|
||||||
options: Rc<Options>,
|
options: Rc<Options>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
niri_render_elements! {
|
||||||
|
TileRenderElement<R> => {
|
||||||
|
LayoutElement = LayoutElementRenderElement<R>,
|
||||||
|
FocusRing = FocusRingRenderElement,
|
||||||
|
SolidColor = SolidColorRenderElement,
|
||||||
|
Offscreen = RescaleRenderElement<OffscreenRenderElement>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +85,58 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
let draw_border_with_background = self
|
||||||
self.border.update(
|
.window
|
||||||
(width, width).into(),
|
.rules()
|
||||||
self.window.size(),
|
.draw_border_with_background
|
||||||
self.window.has_ssd(),
|
.unwrap_or_else(|| !self.window.has_ssd());
|
||||||
);
|
self.border
|
||||||
|
.update(self.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.tile_size(), !draw_focus_ring_with_background);
|
||||||
|
self.focus_ring.set_active(is_active);
|
||||||
|
|
||||||
|
match &mut self.open_animation {
|
||||||
|
Some(anim) => {
|
||||||
|
anim.set_current_time(current_time);
|
||||||
|
if anim.is_done() {
|
||||||
|
self.open_animation = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn are_animations_ongoing(&self) -> bool {
|
||||||
|
self.open_animation.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_open_animation(&mut self) {
|
||||||
|
self.open_animation = Some(Animation::new(
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
0.,
|
||||||
|
self.options.animations.window_open,
|
||||||
|
niri_config::Animation::default_window_open(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -141,6 +203,23 @@ impl<W: LayoutElement> Tile<W> {
|
|||||||
self.window.size()
|
self.window.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an animated size of the tile for rendering and input.
|
||||||
|
///
|
||||||
|
/// During the window opening animation, windows to the right should gradually slide further to
|
||||||
|
/// the right. This is what this visual size is used for. Other things like window resizes or
|
||||||
|
/// transactions or new view position calculation always use the real size, instead of this
|
||||||
|
/// visual size.
|
||||||
|
pub fn visual_tile_size(&self) -> Size<i32, Logical> {
|
||||||
|
let size = self.tile_size();
|
||||||
|
let v = self
|
||||||
|
.open_animation
|
||||||
|
.as_ref()
|
||||||
|
.map(|anim| anim.value())
|
||||||
|
.unwrap_or(1.)
|
||||||
|
.max(0.);
|
||||||
|
Size::from(((f64::from(size.w) * v).round() as i32, size.h))
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -228,50 +307,113 @@ 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>,
|
||||||
where
|
focus_ring: bool,
|
||||||
<R as Renderer>::TextureId: 'static,
|
target: RenderTarget,
|
||||||
{
|
) -> impl Iterator<Item = TileRenderElement<R>> {
|
||||||
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 rv = self
|
||||||
rv.extend(self.window.render(renderer, window_pos, scale));
|
.window
|
||||||
|
.render(renderer, location + self.window_loc(), scale, alpha, target)
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into);
|
||||||
|
|
||||||
if self.effective_border_width().is_some() {
|
let elem = self.effective_border_width().map(|width| {
|
||||||
rv.extend(
|
self.border
|
||||||
self.border
|
.render(
|
||||||
.render(scale)
|
renderer,
|
||||||
.map(|elem| {
|
location + Point::from((width, width)),
|
||||||
RelocateRenderElement::from_element(
|
scale,
|
||||||
elem,
|
view_size,
|
||||||
location.to_physical_precise_round(scale),
|
)
|
||||||
Relocate::Relative,
|
.map(Into::into)
|
||||||
)
|
});
|
||||||
})
|
let rv = rv.chain(elem.into_iter().flatten());
|
||||||
.map(Into::into),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_fullscreen {
|
let elem = focus_ring.then(|| {
|
||||||
let elem = SolidColorRenderElement::from_buffer(
|
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,
|
&self.fullscreen_backdrop,
|
||||||
location.to_physical_precise_round(scale),
|
location.to_physical_precise_round(scale),
|
||||||
scale,
|
scale,
|
||||||
1.,
|
1.,
|
||||||
Kind::Unspecified,
|
Kind::Unspecified,
|
||||||
);
|
)
|
||||||
rv.push(elem.into());
|
.into()
|
||||||
}
|
});
|
||||||
|
rv.chain(elem)
|
||||||
|
}
|
||||||
|
|
||||||
rv
|
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.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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+714
-226
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(())
|
||||||
|
}
|
||||||
|
|||||||
+1127
-661
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,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;
|
||||||
|
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>,
|
||||||
|
) -> Vec<Rectangle<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,180 @@
|
|||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use anyhow::{ensure, Context};
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::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::{Physical, Rectangle, Scale, Size, Transform};
|
||||||
|
use smithay::wayland::shm;
|
||||||
|
|
||||||
|
pub mod gradient;
|
||||||
|
pub mod offscreen;
|
||||||
|
pub mod primary_gpu_pixel_shader;
|
||||||
|
pub mod primary_gpu_texture;
|
||||||
|
pub mod render_elements;
|
||||||
|
pub mod renderer;
|
||||||
|
pub mod shaders;
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (_, sync_point) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
|
||||||
|
sync_point.wait();
|
||||||
|
|
||||||
|
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;
|
||||||
|
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>,
|
||||||
|
) -> Vec<Rectangle<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;
|
||||||
|
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::renderer::AsGlesFrame;
|
||||||
|
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||||
|
|
||||||
|
/// Wrapper for a poxel 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>,
|
||||||
|
) -> Vec<Rectangle<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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
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>);
|
||||||
@@ -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>,
|
||||||
|
) -> Vec<smithay::utils::Rectangle<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,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,46 @@
|
|||||||
|
use smithay::backend::renderer::gles::{GlesPixelProgram, GlesRenderer, UniformName, UniformType};
|
||||||
|
|
||||||
|
use super::renderer::NiriRenderer;
|
||||||
|
|
||||||
|
pub struct Shaders {
|
||||||
|
pub gradient_border: Option<GlesPixelProgram>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
Self { gradient_border }
|
||||||
|
}
|
||||||
|
|
||||||
|
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,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,43 @@ 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,
|
||||||
|
niri_config::Animation::default_config_notification_open_close(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +92,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 +101,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 +119,9 @@ 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() {
|
// HACK: prevent bounciness on hiding. This is better done with a clamp property on
|
||||||
|
// the spring animation.
|
||||||
|
if anim.is_done() || anim.value() <= 0. {
|
||||||
self.state = State::Hidden;
|
self.state = State::Hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,11 +142,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 +185,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 +224,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,275 @@
|
|||||||
|
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 as _, Id, Kind};
|
||||||
|
use smithay::desktop::space::SpaceElement as _;
|
||||||
|
use smithay::desktop::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, Size, Transform};
|
||||||
|
use smithay::wayland::compositor::{send_surface_state, with_states};
|
||||||
|
use smithay::wayland::shell::xdg::{SurfaceCachedState, ToplevelSurface};
|
||||||
|
|
||||||
|
use super::{ResolvedWindowRules, WindowRef};
|
||||||
|
use crate::layout::{LayoutElement, LayoutElementRenderElement};
|
||||||
|
use crate::niri::WindowOffscreenId;
|
||||||
|
use crate::render_helpers::renderer::NiriRenderer;
|
||||||
|
use crate::render_helpers::RenderTarget;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Mapped {
|
||||||
|
pub window: Window,
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapped {
|
||||||
|
pub fn new(window: Window, rules: ResolvedWindowRules) -> Self {
|
||||||
|
Self {
|
||||||
|
window,
|
||||||
|
rules,
|
||||||
|
need_to_recompute_rules: false,
|
||||||
|
is_focused: false,
|
||||||
|
block_out_buffer: RefCell::new(SolidColorBuffer::new((0, 0), [0., 0., 0., 1.])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
self.window.render_elements(
|
||||||
|
renderer,
|
||||||
|
buf_pos.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_size(&self, size: Size<i32, Logical>) {
|
||||||
|
self.toplevel().with_pending_state(|state| {
|
||||||
|
state.size = Some(size);
|
||||||
|
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(&self) {
|
||||||
|
self.toplevel().send_pending_configure();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,161 @@
|
|||||||
|
### 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
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontal-view-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 epsilon=0.0001
|
||||||
|
}
|
||||||
|
|
||||||
|
window-open {
|
||||||
|
duration-ms 150
|
||||||
|
curve "ease-out-expo"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 two curves: `ease-out-cubic` and `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.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `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.
|
||||||
|
- When a window resizes bigger and the camera scrolls to show it in full.
|
||||||
|
- After a horizontal touchpad gesture (a spring is recommended).
|
||||||
|
|
||||||
|
```
|
||||||
|
animations {
|
||||||
|
horizontal-view-movement {
|
||||||
|
spring damping-ratio=1.0 stiffness=800 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
Key bindings are declared in the `binds {}` section of the config.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This is one of the few sections that *does not* get automatically filled with defaults if you omit it, so make sure to copy it from the default config.
|
||||||
|
|
||||||
|
Each bind is a hotkey followed by one action enclosed in curly brackets.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+Left { focus-column-left; }
|
||||||
|
Super+Alt+L { spawn "swaylock"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The hotkey consists of modifiers separated by `+` signs, followed by an XKB key name in the end.
|
||||||
|
|
||||||
|
Valid modifiers are:
|
||||||
|
|
||||||
|
- `Ctrl` or `Control`;
|
||||||
|
- `Shift`;
|
||||||
|
- `Alt`;
|
||||||
|
- `Super` or `Win`;
|
||||||
|
- `ISO_Level3_Shift` or `Mod5`—this is the AltGr key on certain layouts;
|
||||||
|
- `Mod`.
|
||||||
|
|
||||||
|
`Mod` is a special modifier that is equal to `Super` when running niri on a TTY, and to `Alt` when running niri as a nested winit window.
|
||||||
|
This way, you can test niri in a window without causing too many conflicts with the host compositor's key bindings.
|
||||||
|
For this reason, most of the default keys use the `Mod` modifier.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> To find an XKB name for a particular key, you may use a program like [`wev`](https://git.sr.ht/~sircmpwn/wev).
|
||||||
|
>
|
||||||
|
> Open it from a terminal and press the key that you want to detect.
|
||||||
|
> In the terminal, you will see output like this:
|
||||||
|
>
|
||||||
|
> ```
|
||||||
|
> [14: wl_keyboard] key: serial: 757775; time: 44940343; key: 113; state: 1 (pressed)
|
||||||
|
> sym: Left (65361), utf8: ''
|
||||||
|
> [14: wl_keyboard] key: serial: 757776; time: 44940432; key: 113; state: 0 (released)
|
||||||
|
> sym: Left (65361), utf8: ''
|
||||||
|
> [14: wl_keyboard] key: serial: 757777; time: 44940753; key: 114; state: 1 (pressed)
|
||||||
|
> sym: Right (65363), utf8: ''
|
||||||
|
> [14: wl_keyboard] key: serial: 757778; time: 44940846; key: 114; state: 0 (released)
|
||||||
|
> sym: Right (65363), utf8: ''
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Here, look at `sym: Left` and `sym: Right`: these are the key names.
|
||||||
|
> I was pressing the left and the right arrow in this example.
|
||||||
|
|
||||||
|
Binds can also have a cooldown, which will rate-limit the bind and prevent it from repeatedly triggering too quickly.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+T cooldown-ms=500 { spawn "alacritty"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is mostly useful for the scroll bindings.
|
||||||
|
|
||||||
|
### Scroll Bindings
|
||||||
|
|
||||||
|
You can bind mouse wheel scroll ticks using the following syntax.
|
||||||
|
These binds will change direction based on the `natural-scroll` setting.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||||
|
Mod+WheelScrollRight { focus-column-right; }
|
||||||
|
Mod+WheelScrollLeft { focus-column-left; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, you can bind touchpad scroll "ticks".
|
||||||
|
Touchpad scrolling is continuous, so for these binds it is split into discrete intervals based on distance travelled.
|
||||||
|
|
||||||
|
These binds are also affected by touchpad's `natural-scroll`, so these example binds are "inverted", since niri has `natural-scroll` enabled for touchpads by default.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; }
|
||||||
|
Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both mouse wheel and touchpad scroll binds will prevent applications from receiving any scroll events when their modifiers are held down.
|
||||||
|
For example, if you have a `Mod+WheelScrollDown` bind, then while holding `Mod`, all mouse wheel scrolling will be consumed by niri.
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
Every action that you can bind is also available for programmatic invocation via `niri msg action`.
|
||||||
|
Run `niri msg action` to get a full list of actions along with their short descriptions.
|
||||||
|
|
||||||
|
Here are a few actions that benefit from more explanation.
|
||||||
|
|
||||||
|
#### `spawn`
|
||||||
|
|
||||||
|
Run a program.
|
||||||
|
|
||||||
|
`spawn` accepts a path to the program binary as the first argument, followed by arguments to the program.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
// Run alacritty.
|
||||||
|
Mod+T { spawn "alacritty"; }
|
||||||
|
|
||||||
|
// Run `wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1+`.
|
||||||
|
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently, niri *does not* use a shell to run commands, which means that you need to manually separate arguments.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
// Correct: every argument is in its own quotes.
|
||||||
|
Mod+T { spawn "alacritty" "-e" "/usr/bin/fish"; }
|
||||||
|
|
||||||
|
// Wrong: will interpret the whole `alacritty -e /usr/bin/fish` string as the binary path.
|
||||||
|
Mod+T { spawn "alacritty -e /usr/bin/fish"; }
|
||||||
|
|
||||||
|
// Wrong: will pass `-e /usr/bin/fish` as one argument, which alacritty won't understand.
|
||||||
|
Mod+T { spawn "alacritty" "-e /usr/bin/fish"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This also means that you cannot expand environment variables or `~`.
|
||||||
|
If you need this, you can run the command through a shell manually.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
// Wrong: no shell expansion here. These strings will be passed literally to the program.
|
||||||
|
Mod+T { spawn "grim" "-o" "$MAIN_OUTPUT" "~/screenshot.png"; }
|
||||||
|
|
||||||
|
// Correct: run this through a shell manually so that it can expand the arguments.
|
||||||
|
// Note that the entire command is passed as a SINGLE argument,
|
||||||
|
// because shell will do its own argument splitting by whitespace.
|
||||||
|
Mod+T { spawn "sh" "-c" "grim -o $MAIN_OUTPUT ~/screenshot.png"; }
|
||||||
|
|
||||||
|
// You can also use a shell to run multiple commands,
|
||||||
|
// use pipes, process substitution, and so on.
|
||||||
|
Mod+T { spawn "sh" "-c" "notify-send clipboard \"$(wl-paste)\""; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As a special case, niri will expand `~` to the home directory *only* at the beginning of the program name.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
// This will work: one ~ at the very beginning.
|
||||||
|
Mod+T { spawn "~/scripts/do-something.sh"; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `quit`
|
||||||
|
|
||||||
|
Exit niri after showing a confirmation dialog to avoid accidentally triggering it.
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+Shift+E { quit; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to skip the confirmation dialog, set the flag like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
binds {
|
||||||
|
Mod+Shift+E { quit skip-confirmation=true; }
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
In the `layout {}` section you can change various settings that influence how windows are positioned and sized.
|
||||||
|
|
||||||
|
Here are the contents of this section at a glance:
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
gaps 16
|
||||||
|
center-focused-column "never"
|
||||||
|
|
||||||
|
preset-column-widths {
|
||||||
|
proportion 0.33333
|
||||||
|
proportion 0.5
|
||||||
|
proportion 0.66667
|
||||||
|
}
|
||||||
|
|
||||||
|
default-column-width { proportion 0.5; }
|
||||||
|
|
||||||
|
focus-ring {
|
||||||
|
// off
|
||||||
|
width 4
|
||||||
|
active-color "#7fc8ff"
|
||||||
|
inactive-color "#505050"
|
||||||
|
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||||
|
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
|
||||||
|
}
|
||||||
|
|
||||||
|
border {
|
||||||
|
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 {
|
||||||
|
// left 64
|
||||||
|
// right 64
|
||||||
|
// top 64
|
||||||
|
// bottom 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `gaps`
|
||||||
|
|
||||||
|
Set gaps around (inside and outside) windows in logical pixels.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
gaps 16
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `center-focused-column`
|
||||||
|
|
||||||
|
When to center a column when changing focus.
|
||||||
|
This can be set to:
|
||||||
|
|
||||||
|
- `"never"`: no special centering, focusing an off-screen column will scroll it to the left or right edge of the screen. This is the default.
|
||||||
|
- `"always"`, the focused column will always be centered.
|
||||||
|
- `"on-overflow"`, focusing a column will center it if it doesn't fit on screen together with the previously focused column.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
center-focused-column "always"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `preset-column-widths`
|
||||||
|
|
||||||
|
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
|
||||||
|
|
||||||
|
`proportion` sets the width as a fraction of the output width, taking gaps into account.
|
||||||
|
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
|
||||||
|
The default preset widths are <sup>1</sup>⁄<sub>3</sub>, <sup>1</sup>⁄<sub>2</sub> and <sup>2</sup>⁄<sub>3</sub> of the output.
|
||||||
|
|
||||||
|
`fixed` sets the width in logical pixels exactly.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
// Cycle between 1/3, 1/2, 2/3 of the output, and a fixed 1280 logical pixels.
|
||||||
|
preset-column-widths {
|
||||||
|
proportion 0.33333
|
||||||
|
proportion 0.5
|
||||||
|
proportion 0.66667
|
||||||
|
fixed 1280
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Currently, due to an oversight, a preset `fixed` width does not take borders into account.
|
||||||
|
> I.e., preset `fixed 1000` with 4-wide borders will make the window 992 logical pixels wide.
|
||||||
|
> This may eventually be corrected.
|
||||||
|
>
|
||||||
|
> All other ways of using `fixed` (i.e. `default-column-width` or `set-column-width`) do take borders into account and give you the exact window width that you request.
|
||||||
|
|
||||||
|
### `default-column-width`
|
||||||
|
|
||||||
|
Set the default width of the new windows.
|
||||||
|
|
||||||
|
The syntax is the same as in `preset-column-widths` above.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
// Open new windows sized 1/3 of the output.
|
||||||
|
default-column-width { proportion 0.33333; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also leave the brackets empty, then the windows themselves will decide their initial width.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
// New windows decide their initial width themselves.
|
||||||
|
default-column-width {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `default-column-width {}` causes niri to send a (0, H) size in the initial configure request.
|
||||||
|
>
|
||||||
|
> This is a bit [unclearly defined](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/155) in the Wayland protocol, so some clients may misinterpret it.
|
||||||
|
> In practice, the only problematic client I saw is [foot](https://codeberg.org/dnkl/foot/), which takes this as a request to have a literal zero width.
|
||||||
|
>
|
||||||
|
> Either way, `default-column-width {}` is most useful for specific windows, in form of a [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules) with the same syntax.
|
||||||
|
|
||||||
|
### `focus-ring` and `border`
|
||||||
|
|
||||||
|
Focus ring and border are drawn around windows and indicate the active window.
|
||||||
|
They are very similar and have the same options.
|
||||||
|
|
||||||
|
The difference is that the focus ring is drawn only around the active window, whereas borders are drawn around all windows and affect their sizes (windows shrink to make space for the borders).
|
||||||
|
|
||||||
|
| Focus Ring | Border |
|
||||||
|
| ---------- | ------ |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> By default focus ring and border are rendered as a solid background rectangle 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 the `prefer-no-csd` setting at the [top level](./Configuration:-Miscellaneous.md) of the config.
|
||||||
|
> Niri will draw focus rings and borders *around* windows that agree to omit their client-side decorations.
|
||||||
|
>
|
||||||
|
> Alternatively, you can override this behavior with the `draw-border-with-background` [window rule](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules).
|
||||||
|
|
||||||
|
Focus ring and border have the following options.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
// focus-ring has the same options.
|
||||||
|
border {
|
||||||
|
// Uncomment this line to disable the border.
|
||||||
|
// off
|
||||||
|
|
||||||
|
// Width of the border in logical pixels.
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Colors
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
`active-color` is the color of the focus ring / border around the active window, and `inactive-color` is the color of the focus ring / border around all other windows.
|
||||||
|
|
||||||
|
The *focus ring* is only drawn around the active window on each monitor, so with a single monitor you will never see its `inactive-color`.
|
||||||
|
You will see it if you have multiple monitors, though.
|
||||||
|
|
||||||
|
There's also a *deprecated* syntax for setting colors with four numbers representing R, G, B and A: `active-color 127 200 255 255`.
|
||||||
|
|
||||||
|
#### Gradients
|
||||||
|
|
||||||
|
Similarly to colors, you can set `active-gradient` and `inactive-gradient`, which will take precedence.
|
||||||
|
|
||||||
|
Gradients are rendered the same as CSS [`linear-gradient(angle, from, to)`](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient).
|
||||||
|
The angle works 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, like [this one](https://www.css-gradient.com/).
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
focus-ring {
|
||||||
|
active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Gradients can be colored relative to windows individually (the default), or to the whole view of the workspace.
|
||||||
|
To do that, set `relative-to="workspace-view"`.
|
||||||
|
Here's a visual example:
|
||||||
|
|
||||||
|
| Default | `relative-to="workspace-view"` |
|
||||||
|
| --- | --- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
border {
|
||||||
|
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`
|
||||||
|
|
||||||
|
Struts shrink the area occupied by windows, similarly to layer-shell panels.
|
||||||
|
You can think of them as a kind of outer gaps.
|
||||||
|
They are set in logical pixels.
|
||||||
|
|
||||||
|
Left and right struts will cause the next window to the side to always peek out slightly.
|
||||||
|
Top and bottom struts will simply add outer gaps in addition to the area occupied by layer-shell panels and regular gaps.
|
||||||
|
|
||||||
|
```
|
||||||
|
layout {
|
||||||
|
struts {
|
||||||
|
left 64
|
||||||
|
right 64
|
||||||
|
top 64
|
||||||
|
bottom 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
This page documents all top-level options that don't otherwise have dedicated pages.
|
||||||
|
|
||||||
|
Here are all of these options at a glance:
|
||||||
|
|
||||||
|
```
|
||||||
|
spawn-at-startup "waybar"
|
||||||
|
spawn-at-startup "alacritty"
|
||||||
|
|
||||||
|
prefer-no-csd
|
||||||
|
|
||||||
|
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||||
|
|
||||||
|
environment {
|
||||||
|
QT_QPA_PLATFORM "wayland"
|
||||||
|
DISPLAY null
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor {
|
||||||
|
xcursor-theme "breeze_cursors"
|
||||||
|
xcursor-size 48
|
||||||
|
}
|
||||||
|
|
||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `spawn-at-startup`
|
||||||
|
|
||||||
|
Add lines like this to spawn processes at niri startup.
|
||||||
|
|
||||||
|
`spawn-at-startup` accepts a path to the program binary as the first argument, followed by arguments to the program.
|
||||||
|
|
||||||
|
This option works the same way as the `spawn` key binding action, so please read about all its subtleties on the [key bindings](./Configuration:-Key-Bindings.md) page.
|
||||||
|
|
||||||
|
```
|
||||||
|
spawn-at-startup "waybar"
|
||||||
|
spawn-at-startup "alacritty"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that running niri as a systemd session supports xdg-desktop-autostart out of the box, which may be more convenient to use.
|
||||||
|
Thanks to this, apps that you configured to autostart in GNOME will also "just work" in niri, without any manual `spawn-at-startup` configuration.
|
||||||
|
|
||||||
|
### `prefer-no-csd`
|
||||||
|
|
||||||
|
This flag will make niri ask the applications to omit their client-side decorations.
|
||||||
|
|
||||||
|
If an application will specifically ask for CSD, the request will be honored.
|
||||||
|
Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||||
|
|
||||||
|
With `prefer-no-csd` set, applications that negotiate server-side decorations through the xdg-decoration protocol will have focus ring and border drawn around them *without* a solid colored background.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Unlike most other options, changing `prefer-no-csd` will not affect already running applications.
|
||||||
|
> This mainly has to do with niri working around a [bug in SDL2](https://github.com/libsdl-org/SDL/issues/8173) that prevents SDL2 applications from starting.
|
||||||
|
>
|
||||||
|
> Restart applications after changing `prefer-no-csd` in the config to apply it.
|
||||||
|
|
||||||
|
```
|
||||||
|
prefer-no-csd
|
||||||
|
```
|
||||||
|
|
||||||
|
### `screenshot-path`
|
||||||
|
|
||||||
|
Set the path where screenshots are saved.
|
||||||
|
A `~` at the front will be expanded to the home directory.
|
||||||
|
|
||||||
|
The path is formatted with `strftime(3)` to give you the screenshot date and time.
|
||||||
|
|
||||||
|
Niri will create the last folder of the path if it doesn't exist.
|
||||||
|
|
||||||
|
```
|
||||||
|
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set this option to `null` to disable saving screenshots to disk.
|
||||||
|
|
||||||
|
```
|
||||||
|
screenshot-path null
|
||||||
|
```
|
||||||
|
|
||||||
|
### `environment`
|
||||||
|
|
||||||
|
Override environment variables for processes spawned by niri.
|
||||||
|
|
||||||
|
```
|
||||||
|
environment {
|
||||||
|
// Set a variable like this:
|
||||||
|
// QT_QPA_PLATFORM "wayland"
|
||||||
|
|
||||||
|
// Remove a variable by using null as the value:
|
||||||
|
// DISPLAY null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `cursor`
|
||||||
|
|
||||||
|
Change the theme and size of the cursor as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables.
|
||||||
|
|
||||||
|
```
|
||||||
|
cursor {
|
||||||
|
xcursor-theme "breeze_cursors"
|
||||||
|
xcursor-size 48
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `hotkey-overlay`
|
||||||
|
|
||||||
|
Settings for the "Important Hotkeys" overlay.
|
||||||
|
|
||||||
|
Set the `skip-at-startup` flag if you don't want to see the hotkey help at niri startup.
|
||||||
|
|
||||||
|
```
|
||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
By default, niri will attempt to turn on all connected monitors using their preferred modes.
|
||||||
|
You can disable or adjust this with `output` sections.
|
||||||
|
|
||||||
|
Here's what it looks like with all properties written out:
|
||||||
|
|
||||||
|
```
|
||||||
|
output "eDP-1" {
|
||||||
|
// off
|
||||||
|
mode "1920x1080@120.030"
|
||||||
|
scale 2.0
|
||||||
|
transform "90"
|
||||||
|
position x=1280 y=0
|
||||||
|
}
|
||||||
|
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
// ...settings for HDMI-A-1...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs are matched by connector name (i.e. `eDP-1`, `HDMI-A-1`) which you can find by running `niri msg outputs`.
|
||||||
|
Usually, the built-in monitor in laptops will be called `eDP-1`.
|
||||||
|
Matching by output manufacturer and model is planned, but blocked on Smithay adopting libdisplay-info instead of edid-rs.
|
||||||
|
|
||||||
|
### `off`
|
||||||
|
|
||||||
|
This flag turns off that output entirely.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Turn off that monitor.
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mode`
|
||||||
|
|
||||||
|
Set the monitor resolution and 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 for the resolution.
|
||||||
|
|
||||||
|
If the mode is omitted altogether or doesn't work, niri will try to pick one automatically.
|
||||||
|
|
||||||
|
Run `niri msg outputs` while inside a niri instance to list all outputs and their modes.
|
||||||
|
The refresh rate that you set here must match *exactly*, down to the three decimal digits, to what you see in `niri msg outputs`.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Set a high refresh rate for this monitor.
|
||||||
|
// High refresh rate monitors tend to use 60 Hz as their preferred mode,
|
||||||
|
// requiring a manual mode setting.
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
mode "2560x1440@143.912"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a lower resolution on the built-in laptop monitor
|
||||||
|
// (for example, for testing purposes).
|
||||||
|
output "eDP-1" {
|
||||||
|
mode "1280x720"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scale`
|
||||||
|
|
||||||
|
Set the scale of the monitor.
|
||||||
|
|
||||||
|
This is a floating-point number to enable fractional scaling in the future, but at the moment only integer scale values will work.
|
||||||
|
|
||||||
|
```
|
||||||
|
output "eDP-1" {
|
||||||
|
scale 2.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `transform`
|
||||||
|
|
||||||
|
Rotate the output counter-clockwise.
|
||||||
|
|
||||||
|
Valid values are: `"normal"`, `"90"`, `"180"`, `"270"`, `"flipped"`, `"flipped-90"`, `"flipped-180"` and `"flipped-270"`.
|
||||||
|
Values with `flipped` additionally flip the output.
|
||||||
|
|
||||||
|
```
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
transform "90"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `position`
|
||||||
|
|
||||||
|
Set the position of the output in the global coordinate space.
|
||||||
|
|
||||||
|
This affects directional monitor actions like `focus-monitor-left`, and cursor movement.
|
||||||
|
The cursor can only move between directly adjacent outputs.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Output scale and rotation has to be taken into account for positioning: 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, so to put another output directly adjacent to it on the right, set its x to 1920.
|
||||||
|
> If the position is unset or results in an overlap, the output is instead placed automatically.
|
||||||
|
|
||||||
|
```
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
position x=1280 y=0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Automatic Positioning
|
||||||
|
|
||||||
|
Niri repositions outputs from scratch every time the output configuration changes (which includes monitors disconnecting and connecting).
|
||||||
|
The following algorithm is used for positioning outputs.
|
||||||
|
|
||||||
|
1. Collect all connected monitors and their logical sizes.
|
||||||
|
1. Sort them by their name. This makes it so the automatic positioning does not depend on the order the monitors are connected. This is important because the connection order is non-deterministic at compositor startup.
|
||||||
|
1. Try to place every output with explicitly configured `position`, in order. If the output overlaps previously placed outputs, place it to the right of all previously placed outputs. In this case, niri will also print a warning.
|
||||||
|
1. Place every output without explicitly configured `position` by putting it to the right of all previously placed outputs.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
### Per-Section Documentation
|
||||||
|
|
||||||
|
You can find documentation for various sections of the config on these wiki pages:
|
||||||
|
|
||||||
|
* [`input {}`](./Configuration:-Input.md)
|
||||||
|
* [`output "eDP-1" {}`](./Configuration:-Outputs.md)
|
||||||
|
* [`binds {}`](./Configuration:-Key-Bindings.md)
|
||||||
|
* [`layout {}`](./Configuration:-Layout.md)
|
||||||
|
* [top-level options](./Configuration:-Miscellaneous.md)
|
||||||
|
* [`window-rule {}`](./Configuration:-Window-Rules.md)
|
||||||
|
* [`animations {}`](./Configuration:-Animations.md)
|
||||||
|
* [`debug {}`](./Configuration:-Debug-Options.md)
|
||||||
|
|
||||||
|
### Loading
|
||||||
|
|
||||||
|
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
|
||||||
|
If that file is missing, niri will create it with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl).
|
||||||
|
Please use the default configuration file as the starting point for your custom configuration.
|
||||||
|
|
||||||
|
The configuration is live-reloaded.
|
||||||
|
Simply edit and save the config file, and your changes will be applied.
|
||||||
|
This includes key bindings, output settings like mode, window rules, and everything else.
|
||||||
|
|
||||||
|
You can run `niri validate` to parse the config and see any errors.
|
||||||
|
|
||||||
|
To use a different config file path, pass it in the `--config` or `-c` argument to `niri`.
|
||||||
|
|
||||||
|
### Syntax
|
||||||
|
|
||||||
|
The config is written in [KDL].
|
||||||
|
|
||||||
|
#### Comments
|
||||||
|
|
||||||
|
Lines starting with `//` are comments; they are ignored.
|
||||||
|
|
||||||
|
Also, you can put `/-` in front of a section to comment out the entire section:
|
||||||
|
|
||||||
|
```
|
||||||
|
/-output "eDP-1" {
|
||||||
|
everything inside here
|
||||||
|
is ignored
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
Toggle options in niri are commonly represented as flags.
|
||||||
|
Writing out the flag enables it, and omitting it or commenting it out disables it.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
// "Focus follows mouse" is enabled.
|
||||||
|
input {
|
||||||
|
focus-follows-mouse
|
||||||
|
|
||||||
|
// Other settings...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
// "Focus follows mouse" is disabled.
|
||||||
|
input {
|
||||||
|
// focus-follows-mouse
|
||||||
|
|
||||||
|
// Other settings...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sections
|
||||||
|
|
||||||
|
Most sections cannot be repeated. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
// This is valid: every section appears once.
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
touchpad {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
// This is NOT valid: input section appears twice.
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
touchpad {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exceptions are, for example, sections that configure different devices by name:
|
||||||
|
|
||||||
|
```
|
||||||
|
output "eDP-1" {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is valid: this section configures a different output.
|
||||||
|
output "HDMI-A-1" {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is NOT valid: "eDP-1" already appeared above.
|
||||||
|
// It will either throw a config parsing error, or otherwise not work.
|
||||||
|
output "eDP-1" {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Defaults
|
||||||
|
|
||||||
|
Omitting most of the sections of the config file will leave you with the default values for that section.
|
||||||
|
A notable exception is `binds {}`: they do not get filled with defaults, so make sure you do not erase this section.
|
||||||
|
|
||||||
|
### Breaking Change Policy
|
||||||
|
|
||||||
|
Configuration backwards compatibility follows the Rust / Cargo semantic versioning standards.
|
||||||
|
A patch release (i.e. niri 0.1.3 to 0.1.4) must not cause a parse error on a config that worked on the previous version.
|
||||||
|
|
||||||
|
A minor release (i.e. niri 0.1.3 to 0.2.0) *can* cause previously valid config files to stop parsing.
|
||||||
|
When niri reaches 1.0, a major release (i.e. niri 1.0 to 2.0) will be required to break config backwards compatibility.
|
||||||
|
|
||||||
|
Exceptions can be made for parsing bugs.
|
||||||
|
For example, niri used to accept multiple binds to the same key, but this was not intended and did not do anything (the first bind was always used).
|
||||||
|
A patch release changed niri from silently accepting this to causing a parsing failure.
|
||||||
|
This is not a blanket rule, I will consider the potential impact of every breaking change like this before deciding to carry on with it.
|
||||||
|
|
||||||
|
Keep in mind that the breaking change policy applies only to niri releases.
|
||||||
|
Commits between releases can and do occasionally break the config as new features are ironed out.
|
||||||
|
However, I do try to limit these, since several people are running git builds.
|
||||||
|
|
||||||
|
[KDL]: https://kdl.dev/
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
### Overview
|
||||||
|
|
||||||
|
Window rules let you adjust behavior for individual windows.
|
||||||
|
They have `match` and `exclude` directives that control which windows the rule should apply to, and a number of properties that you can set.
|
||||||
|
|
||||||
|
Window rules are processed in order of appearance in the config file.
|
||||||
|
This means that you can put more generic rules first, then override them for specific windows later.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Set open-maximized to true for all windows.
|
||||||
|
window-rule {
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, for Alacritty, set open-maximized back to false.
|
||||||
|
window-rule {
|
||||||
|
match app-id="Alacritty"
|
||||||
|
open-maximized false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> In general, you cannot "unset" a property in a later rule, only set it to a different value.
|
||||||
|
> Use the `exclude` directives to avoid applying a rule for specific windows.
|
||||||
|
|
||||||
|
Here are all matchers and properties that a window rule could have:
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
match title="Firefox"
|
||||||
|
match app-id="Alacritty"
|
||||||
|
match is-active=true
|
||||||
|
match is-focused=false
|
||||||
|
|
||||||
|
// Properties that apply once upon window opening.
|
||||||
|
default-column-width { proportion 0.75; }
|
||||||
|
open-on-output "eDP-1"
|
||||||
|
open-maximized true
|
||||||
|
open-fullscreen true
|
||||||
|
|
||||||
|
// Properties that apply continuously.
|
||||||
|
draw-border-with-background false
|
||||||
|
opacity 0.5
|
||||||
|
block-out-from "screencast"
|
||||||
|
// block-out-from "screen-capture"
|
||||||
|
|
||||||
|
min-width 100
|
||||||
|
max-width 200
|
||||||
|
min-height 300
|
||||||
|
max-height 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Window Matching
|
||||||
|
|
||||||
|
Each window rule can have several `match` and `exclude` directives.
|
||||||
|
In order for the rule to apply, a window needs to match *any* of the `match` directives, and *none* of the `exclude` directives.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
// Match all Telegram windows...
|
||||||
|
match app-id=r#"^org\.telegram\.desktop$"#
|
||||||
|
|
||||||
|
// ...except the media viewer window.
|
||||||
|
exclude title="^Media viewer$"
|
||||||
|
|
||||||
|
// Properties to apply.
|
||||||
|
open-on-output "HDMI-A-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Match and exclude directives have the same syntax.
|
||||||
|
There can be multiple *matchers* in one directive, then the window should match all of them for the directive to apply.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
// Match Firefox windows with Gmail in title.
|
||||||
|
match app-id="org.mozilla.firefox" title="Gmail"
|
||||||
|
}
|
||||||
|
|
||||||
|
window-rule {
|
||||||
|
// Match Firefox, but only when it is active...
|
||||||
|
match app-id=r#"^org\.mozilla\.firefox$"# is-active=true
|
||||||
|
|
||||||
|
// ...or match Telegram...
|
||||||
|
match app-id=r#"^org\.telegram\.desktop$"#
|
||||||
|
|
||||||
|
// ...but don't match the Telegram media viewer.
|
||||||
|
// If you open a tab in Firefox titled "Media viewer",
|
||||||
|
// it will not be excluded because it doesn't match the app-id
|
||||||
|
// of this exclude directive.
|
||||||
|
exclude app-id=r#"^org\.telegram\.desktop$"# title="Media viewer"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's look at the matchers in more detail.
|
||||||
|
|
||||||
|
#### `title` and `app-id`
|
||||||
|
|
||||||
|
These are regular expressions that should match anywhere in the window title and app ID respectively.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Match windows with title containing "Mozilla Firefox",
|
||||||
|
// or windows with app ID containing "Alacritty".
|
||||||
|
window-rule {
|
||||||
|
match title="Mozilla Firefox"
|
||||||
|
match app-id="Alacritty"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Raw KDL strings can be helpful for writing out regular expressions:
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
exclude app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find the title and the app ID of the currently focused window by running `niri msg focused-window`.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Another way to find the window title and app ID is to configure the `wlr/taskbar` module in [Waybar](https://github.com/Alexays/Waybar) to include them in the tooltip:
|
||||||
|
>
|
||||||
|
> ```json
|
||||||
|
> "wlr/taskbar": {
|
||||||
|
> "tooltip-format": "{title} | {app_id}",
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
#### `is-active`
|
||||||
|
|
||||||
|
Can be `true` or `false`.
|
||||||
|
Matches active windows (same windows that have the active border / focus ring color).
|
||||||
|
|
||||||
|
Every workspace on the focused monitor will have one active window.
|
||||||
|
This means that you will usually have multiple active windows (one per workspace), and when you switch between workspaces, you can see two active windows at once.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
match is-active=true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `is-focused`
|
||||||
|
|
||||||
|
Can be `true` or `false`.
|
||||||
|
Matches the window that has the keyboard focus.
|
||||||
|
|
||||||
|
Contrary to `is-active`, there can only be a single focused window.
|
||||||
|
Also, when opening a layer-shell application launcher or pop-up menu, the keyboard focus goes to layer-shell.
|
||||||
|
While layer-shell has the keyboard focus, windows will not match this rule.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
match is-focused=true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Window Opening Properties
|
||||||
|
|
||||||
|
These properties apply once, when a window first opens.
|
||||||
|
|
||||||
|
To be precise, they apply at the point when niri sends the initial configure request to the window.
|
||||||
|
|
||||||
|
#### `default-column-width`
|
||||||
|
|
||||||
|
Set the default width for the new window.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Give Blender and GIMP some guaranteed width on opening.
|
||||||
|
window-rule {
|
||||||
|
match app-id="^blender$"
|
||||||
|
|
||||||
|
// GIMP app ID contains the version like "gimp-2.99",
|
||||||
|
// so we only match the beginning (with ^) and not the end.
|
||||||
|
match app-id="^gimp"
|
||||||
|
|
||||||
|
default-column-width { fixed 1200; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `open-on-output`
|
||||||
|
|
||||||
|
Make the window open on a specific output.
|
||||||
|
|
||||||
|
If such an output does not exist, the window will open on the currently focused output as usual.
|
||||||
|
|
||||||
|
If the window opens on an output that is not currently focused, the window will not be automatically focused.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Open Firefox and Telegram (but not its Media Viewer)
|
||||||
|
// on a specific monitor.
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.mozilla\.firefox$"#
|
||||||
|
match app-id=r#"^org\.telegram\.desktop$"#
|
||||||
|
exclude app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$"
|
||||||
|
|
||||||
|
open-on-output "HDMI-A-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `open-maximized`
|
||||||
|
|
||||||
|
Make the window open as a maximized column.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Maximize Firefox by default.
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.mozilla\.firefox$"#
|
||||||
|
|
||||||
|
open-maximized true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `open-fullscreen`
|
||||||
|
|
||||||
|
Make the window open fullscreen.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
open-fullscreen true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set this to `false` to *prevent* a window from opening fullscreen.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Make the Telegram media viewer open in windowed mode.
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$"
|
||||||
|
|
||||||
|
open-fullscreen false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Properties
|
||||||
|
|
||||||
|
These properties apply continuously to open windows.
|
||||||
|
|
||||||
|
#### `block-out-from`
|
||||||
|
|
||||||
|
You can block out windows from xdg-desktop-portal screencasts.
|
||||||
|
They will be replaced with solid black rectangles.
|
||||||
|
|
||||||
|
This can be useful for password managers or messenger windows, etc.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To preview and set up this rule, check the `preview-render` option in the debug section of the config.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> The window is **not** blocked out from third-party screenshot tools.
|
||||||
|
> If you open some screenshot tool with preview while screencasting, blocked out windows **will be visible** on the screencast.
|
||||||
|
|
||||||
|
The built-in screenshot UI is not affected by this problem though.
|
||||||
|
If you open the screenshot UI while screencasting, you will be able to select the area to screenshot while seeing all windows normally, but on a screencast the selection UI will display with windows blocked out.
|
||||||
|
|
||||||
|
```
|
||||||
|
// Block out password managers from screencasts.
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||||
|
match app-id=r#"^org\.gnome\.World\.Secrets$"#
|
||||||
|
|
||||||
|
block-out-from "screencast"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can block out the window out of *all* screen captures, including third-party screenshot tools.
|
||||||
|
This way you avoid accidentally showing the window on a screencast when opening a third-party screenshot preview.
|
||||||
|
|
||||||
|
This setting will still let you use the interactive built-in screenshot UI, but it will block out the window from the fully automatic screenshot actions, such as `screenshot-screen` and `screenshot-window`.
|
||||||
|
The reasoning is that with an interactive selection, you can make sure that you avoid screenshotting sensitive content.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
block-out-from "screen-capture"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Be careful when blocking out windows based on a dynamically changing window title.
|
||||||
|
>
|
||||||
|
> For example, you might try to block out specific Firefox tabs like this:
|
||||||
|
>
|
||||||
|
> ```
|
||||||
|
> window-rule {
|
||||||
|
> // Doesn't quite work! Try to block out the Gmail tab.
|
||||||
|
> match app-id=r#"^org\.mozilla\.firefox$"# title="- Gmail "
|
||||||
|
>
|
||||||
|
> block-out-from "screencast"
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> It will work, but when switching from a sensitive tab to a regular tab, the contents of the sensitive tab **will show up on a screencast** for an instant.
|
||||||
|
>
|
||||||
|
> This is because window title (and app ID) are not double-buffered in the Wayland protocol, so they are not tied to specific window contents.
|
||||||
|
> There's no robust way for Firefox to synchronize visibly showing a different tab and changing the window title.
|
||||||
|
|
||||||
|
#### `opacity`
|
||||||
|
|
||||||
|
Set the opacity of the window.
|
||||||
|
`0.0` is fully transparent, `1.0` is fully opaque.
|
||||||
|
This is applied on top of the window's own opacity, so semitransparent windows will become even more transparent.
|
||||||
|
|
||||||
|
Opacity is applied to every surface of the window individually, so subsurfaces and pop-up menus will show window content behind them.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Also, focus ring and border with background will show through semitransparent windows (see `prefer-no-csd` and the `draw-border-with-background` window rule below).
|
||||||
|
|
||||||
|
```
|
||||||
|
// Make inactive windows semitransparent.
|
||||||
|
window-rule {
|
||||||
|
match is-active=false
|
||||||
|
|
||||||
|
opacity 0.95
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `draw-border-with-background`
|
||||||
|
|
||||||
|
Override whether the border and the focus ring draw with a background.
|
||||||
|
|
||||||
|
Set this to `true` to draw them as solid colored rectangles even for windows which agreed to omit their client-side decorations.
|
||||||
|
Set this to `false` to draw them as borders around the window even for windows which use client-side decorations.
|
||||||
|
|
||||||
|
This property can be useful for rectangular windows that do not support the xdg-decoration protocol.
|
||||||
|
|
||||||
|
| With Background | Without Background |
|
||||||
|
| --------------- | ------------------ |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
draw-border-with-background false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Size Overrides
|
||||||
|
|
||||||
|
You can amend the window's minimum and maximum size in logical pixels.
|
||||||
|
|
||||||
|
Keep in mind that the window itself always has a final say in its size.
|
||||||
|
These values instruct niri to never ask the window to be smaller than the minimum you set, or to be bigger than the maximum you set.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `max-height` will only apply to automatically-sized windows if it is equal to `min-height`.
|
||||||
|
> Either set it equal to `min-height`, or change the window height manually after opening it with `set-window-height`.
|
||||||
|
>
|
||||||
|
> This is a limitation of niri's window height distribution algorithm.
|
||||||
|
|
||||||
|
```
|
||||||
|
window-rule {
|
||||||
|
min-width 100
|
||||||
|
max-width 200
|
||||||
|
min-height 300
|
||||||
|
max-height 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
// Fix OBS with server-side decorations missing a minimum width.
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^com\.obsproject\.Studio$"#
|
||||||
|
|
||||||
|
min-width 876
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
These are some of the general principles for the design of niri's window layout. They can be sidestepped in specific circumstances if there's a good reason.
|
||||||
|
|
||||||
|
1. Opening a new window should not affect the sizes of any existing windows.
|
||||||
|
2. The focused window should not move around on its own.
|
||||||
|
- In particular: windows opening, closing and resizing to the left of the focused window should not cause it to visually move.
|
||||||
|
3. If a window or popup is larger than the screen, it should be aligned on the top left corner.
|
||||||
|
- The top left area of a window is more likely to contain something important so it should always be visible.
|
||||||
|
4. Setting window width or height to a fixed pixel size (e.g. `set-column-width 1280` or `default-column-width { fixed 1280; }`) will set the size of the window itself, however setting to a proportional size (e.g. `set-column-width 50%`) will set the size of the tile, including the border added by niri.
|
||||||
|
- With proportions, the user is looking to tile multiple windows on screen, so they should include borders.
|
||||||
|
- With fixed sizes, the user wants to test a specific client size or take a specifically sized screenshot, so they should affect the window directly.
|
||||||
|
- After the size is set, it is always converted to a value that includes the borders, to make the code sane. That is, `set-column-width 1000` followed by changing the niri border width will resize the window accordingly.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
## Running a Local Build
|
||||||
|
|
||||||
|
The main way of testing niri during development is running it as a nested window. The second step is usually switching to a different TTY and running niri there.
|
||||||
|
|
||||||
|
Once a feature or fix is reasonably complete, you generally want to run a local build as your main compositor for proper testing. The easiest way to do that is to install niri normally (from a distro package for example), then overwrite the binary with `sudo cp ./target/release/niri /usr/bin/niri`. Do make sure that you know how to revert to a working version in case everything breaks though.
|
||||||
|
|
||||||
|
If you use an RPM-based distro, you can generate an RPM package for a local build with `cargo generate-rpm`.
|
||||||
|
|
||||||
|
## Logging Levels
|
||||||
|
|
||||||
|
Niri uses [`tracing`](https://lib.rs/crates/tracing) for logging. This is how logging levels are used:
|
||||||
|
|
||||||
|
- `error!`: programming errors and bugs that are recoverable. Things you'd normally use `unwrap()` for. However, when a Wayland compositor crashes, it brings down the entire session, so it's better to recover and log an `error!` whenever reasonable. If you see an `ERROR` in the niri log, that always indicates a *bug*.
|
||||||
|
- `warn!`: something bad but still *possible* happened. Informing the user that they did something wrong, or that their hardware did something weird, falls into this category. For example, config parsing errors should be indicated with a `warn!`.
|
||||||
|
- `info!`: the most important messages related to normal operation. Running niri with `RUST_LOG=niri=info` should not make the user want to disable logging altogether.
|
||||||
|
- `debug!`: less important messages related to normal operation. Running niri with `debug!` messages hidden should not negatively impact the UX.
|
||||||
|
- `trace!`: everything that can be useful for debugging but is otherwise too spammy or performance intensive. `trace!` messages are *compiled out* of release builds.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
We have some unit tests, most prominently for the layout code and for config parsing.
|
||||||
|
|
||||||
|
When adding new operations to the layout, add them to the `Op` enum at the bottom of `src/layout/mod.rs` (this will automatically include it in the randomized tests), and if applicable to the `every_op` arrays below.
|
||||||
|
|
||||||
|
When adding new config options, include them in the config parsing test.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
Make sure to run `cargo test --all` to run tests from sub-crates too.
|
||||||
|
|
||||||
|
Some tests are a bit too slow to run normally, like the randomized tests of the layout code, so they are normally skipped. Set the `RUN_SLOW_TESTS` variable to run them:
|
||||||
|
|
||||||
|
```
|
||||||
|
env RUN_SLOW_TESTS=1 cargo test --all
|
||||||
|
```
|
||||||
|
|
||||||
|
It also usually helps to run the randomized tests for a longer period, so that they can explore more inputs. You can control this with environment variables. This is how I usually run tests before pushing:
|
||||||
|
|
||||||
|
```
|
||||||
|
env RUN_SLOW_TESTS=1 PROPTEST_CASES=200000 PROPTEST_MAX_GLOBAL_REJECTS=200000 RUST_BACKTRACE=1 cargo test --release --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Tests
|
||||||
|
|
||||||
|
The `niri-visual-tests` sub-crate is a GTK application that runs hard-coded test cases so that you can visually check that they look right. It uses mock windows with the real layout and rendering code. It is especially helpful when working on animations.
|
||||||
|
|
||||||
|
## Profiling
|
||||||
|
|
||||||
|
We have integration with the [Tracy](https://github.com/wolfpld/tracy) profiler which you can enable by building niri with a feature flag:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo build --release --features=profile-with-tracy
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can open Tracy (you will need the latest stable release) and attach to a running niri instance to collect profiling data. This is **not** currently "on-demand" (until the next Tracy release), so niri will always collect profiling data when compiled this way, and you can't run a build like this as your main compositor.
|
||||||
|
|
||||||
|
To make a niri function show up in Tracy, instrument it like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn some_function() {
|
||||||
|
let _span = tracy_client::span!("some_function");
|
||||||
|
|
||||||
|
// Code of the function.
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
When starting niri from a display manager like GDM, or otherwise through the `niri-session` binary, it runs as a systemd service.
|
||||||
|
This provides the necessary systemd integration to run programs like `mako` and services like `xdg-desktop-portal` bound to the graphical session.
|
||||||
|
|
||||||
|
Here's an example on how you might set up [`mako`](https://github.com/emersion/mako), [`waybar`](https://github.com/Alexays/Waybar), [`swaybg`](https://github.com/swaywm/swaybg) and [`swayidle`](https://github.com/swaywm/swayidle) to run as systemd services with niri.
|
||||||
|
In contrast to the `spawn-at-startup` config option, this lets you easily monitor their status and output, and restart or reload them.
|
||||||
|
|
||||||
|
1. Install them, i.e. `sudo dnf install mako waybar swaybg swayidle`
|
||||||
|
2. Create a `niri.service.wants` folder: `mkdir -p ~/.config/systemd/user/niri.service.wants`
|
||||||
|
|
||||||
|
This is a special systemd folder.
|
||||||
|
Any services linked there will be started together with `niri.service` (which is a systemd unit used by niri when running as a session).
|
||||||
|
|
||||||
|
3. `mako` and `waybar` provide systemd units out of the box, so you can simply symlink them into the `niri.service.wants` folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
ln -s /usr/lib/systemd/user/mako.service ~/.config/systemd/user/niri.service.wants/
|
||||||
|
ln -s /usr/lib/systemd/user/waybar.service ~/.config/systemd/user/niri.service.wants/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. `swaybg` does not provide a systemd unit, since you need to pass the background image as a command-line argument.
|
||||||
|
So we will make our own.
|
||||||
|
Put the following into `~/.config/systemd/user/swaybg.service`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
After=graphical-session.target
|
||||||
|
Requisite=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/swaybg -m fill -i "%h/Pictures/LakeSide.png"
|
||||||
|
Restart=on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the image path with the one you want.
|
||||||
|
`%h` is expanded to your home directory.
|
||||||
|
|
||||||
|
After editing `swaybg.service`, run `systemctl --user daemon-reload` so systemd picks up the changes in the file.
|
||||||
|
|
||||||
|
Now, also symlink this to `niri.service.wants`:
|
||||||
|
|
||||||
|
```
|
||||||
|
ln -s ~/.config/systemd/user/swaybg.service ~/.config/systemd/user/niri.service.wants/
|
||||||
|
```
|
||||||
|
|
||||||
|
5. `swayidle` similarly does not provide a service so we will also make our own. Put the following into `~/.config/systemd/user/swayidle.service`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
After=graphical-session.target
|
||||||
|
Requisite=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/swayidle -w timeout 601 'niri msg action power-off-monitors' timeout 600 'swaylock -f' before-sleep 'swaylock -f'
|
||||||
|
Restart=on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run `systemctl --user daemon-reload` and symlink this file to `niri.service.wants`:
|
||||||
|
|
||||||
|
```
|
||||||
|
ln -s ~/.config/systemd/user/swayidle.service ~/.config/systemd/user/niri.service.wants/
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it!
|
||||||
|
Now these three utilities will be started together with the niri session and stopped when it exits.
|
||||||
|
You can also restart them with a command like `systemctl --user restart waybar.service`, for example after editing their config files.
|
||||||
|
|
||||||
|
### Running Programs Across Logout
|
||||||
|
|
||||||
|
When running niri as a session, exiting it (logging out) will kill all programs that you've started within. However, sometimes you want a program, like `tmux`, `dtach` or similar, to persist in this case. To do this, run it in a transient systemd scope:
|
||||||
|
|
||||||
|
```
|
||||||
|
systemd-run --user --scope tmux new-session
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Welcome to the niri wiki!
|
||||||
|
|
||||||
|
Check out the available pages on the right.
|
||||||
|
|
||||||
|
The wiki is open to contribution, but please discuss bigger changes in [our Matrix room](https://matrix.to/#/#niri:matrix.org) first! The wiki is generated from files in the `wiki/` folder of the repository, so you can open a pull request modifying it there.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user