mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75c79116a7 | |||
| 4f44ef081f | |||
| 4fc76b50d0 | |||
| e1f065ac23 | |||
| 7cc10ce1b5 | |||
| 9d8f640503 | |||
| b18cfbae23 | |||
| f64e7e14c3 | |||
| e8c9bfc06a | |||
| 07452f50a8 | |||
| 642c5acebb | |||
| 0886dedff1 | |||
| cc88a7d42e | |||
| c0829087da | |||
| b6f6d6a7c2 | |||
| 5ff8b89aaf | |||
| 927abad4b4 | |||
| 3d31f9860a | |||
| 8867a4f84c | |||
| 88f4c1d610 | |||
| ddcb5c5e10 | |||
| cd90dfc7be | |||
| a778ab3897 | |||
| 4c2f49d566 | |||
| 49d7052bb3 | |||
| 07be7e7eae | |||
| 97c8717d1e | |||
| 3ac0a751fe | |||
| 8b39f986d9 | |||
| 354c365a03 | |||
| e0ebf1bdff | |||
| 11633aef98 | |||
| 9193245871 | |||
| 7baf10b751 | |||
| f5d91c5ecc | |||
| 69e3edb5a3 | |||
| d58bb4eaa3 | |||
| c5fe25f422 | |||
| 600cffb009 | |||
| b9d14a9eda | |||
| 0e7e398df3 | |||
| 86bdc6898b | |||
| e5ca335115 | |||
| fce5d66878 | |||
| 05d218113c | |||
| ef6af6adc1 | |||
| 6632699e00 | |||
| d3e72245b0 | |||
| 13fe9c8ac3 | |||
| 6ecbf2db8a | |||
| c9be9056ef | |||
| 0866990b7d | |||
| f04befb567 | |||
| da3e5c4424 | |||
| 26ab4dfb87 | |||
| e887ee93a3 | |||
| d640e85158 | |||
| c8044a9b5d | |||
| 289ae3604d | |||
| 55fb885256 | |||
| 73a531f8bc | |||
| 10f04fd19d | |||
| 79fd309d6c | |||
| dd8b2be044 | |||
| 8d08782eba | |||
| 8555f37dbf | |||
| 4b837f429c | |||
| a480087618 | |||
| 84655d3b26 | |||
| 40843cbda1 | |||
| a13b9298c6 | |||
| 0c5e046820 | |||
| 907ebc4977 | |||
| e4161be1bf | |||
| be7fbd418f | |||
| 06ec9eecdb | |||
| 79eef5ee90 | |||
| 29602ca995 | |||
| d7156df842 | |||
| 33b39913c7 | |||
| d5cbc35811 | |||
| a038c5aaab | |||
| c9c985c927 | |||
| 859c0be0e5 | |||
| 810ea245f9 | |||
| 58fc5f3b06 | |||
| 7d4e99b760 | |||
| ab7d81aae0 | |||
| e24723125f | |||
| 03c603918d | |||
| 6fb60dacd2 | |||
| 42a9daec9d | |||
| 1ba2be3928 | |||
| 66be000410 | |||
| 5fc669c282 | |||
| 9b78b15ba5 | |||
| b9fd0a405e | |||
| 1b44e0cd20 | |||
| b3d4d4eacc | |||
| a835bdc940 | |||
| b258fd69d2 | |||
| 3ab3e778ab | |||
| e6203313ce | |||
| 938061dd5e | |||
| 0cca7a2116 | |||
| 39b46b3326 | |||
| 2aebd6bdbb | |||
| b501a9b303 | |||
| 94e5408f46 | |||
| eb190e3f94 | |||
| 80bb0d5876 | |||
| c04ccafd0a | |||
| 6ee5b5afa7 | |||
| 6a48728ffb | |||
| 9cb89ff26c | |||
| 4e5f392c50 | |||
| e35d9e760b | |||
| 22fee7b003 | |||
| e95d28e148 | |||
| 7a65a0b79f | |||
| ca30315deb | |||
| 9538e8f916 | |||
| 8b3715eabf | |||
| d0f2b9abd0 | |||
| 43578e21b1 | |||
| 55a798bd8b | |||
| cdcd5a2835 | |||
| 737e99ec69 | |||
| c3cb42f04d | |||
| d0e624e615 | |||
| 087a50a19c | |||
| 0bed253835 | |||
| 6b6a84e55b | |||
| 7d5785e96f | |||
| 70fa38fadf | |||
| 3514cd2e36 | |||
| 96083847fb | |||
| d25d6ce337 | |||
| bb044075fa | |||
| 370fd4e172 | |||
| 7dea3822a3 | |||
| 7d11ef0abb | |||
| dcb29efce5 | |||
| cb5d97f600 | |||
| 608ab7d8b1 | |||
| fd8ebb9d06 | |||
| 952916fd1c | |||
| a0592e8f53 | |||
| 5460c792bd | |||
| e5ecd27bbe | |||
| 4543873dae | |||
| a2c855315c | |||
| 6c4e4b374a | |||
| 9ab887bec8 | |||
| 268591f343 | |||
| a42717bcac | |||
| 6b013a08fc | |||
| b65a243fc9 | |||
| f0157e03e7 | |||
| 4b7c16b04a | |||
| aafd5ab70f | |||
| d8d6b5a5e0 | |||
| a1fd4b396f | |||
| 5521cdda63 | |||
| 12b16a9d7e | |||
| f7181fb066 | |||
| 17ac52e1d4 | |||
| 64a9351921 | |||
| 332af8b062 | |||
| b7901579d5 | |||
| 138c2a3bfd | |||
| 446a9f1e06 | |||
| 52265e2e19 | |||
| 0f522f209b | |||
| 30b213601a | |||
| 8eb34b2e18 | |||
| 74d1b1f406 | |||
| 2b3d196876 | |||
| 397b7e4bb9 | |||
| 598b27f83c | |||
| da53e79d07 | |||
| 2907d5af3e | |||
| dd919fe01b | |||
| f86a9bed1a | |||
| cfa87d508e | |||
| f19e1711a7 | |||
| 20cd4f5d04 | |||
| b2c7d3ad40 | |||
| 4832924483 | |||
| 28a8a9ace2 | |||
| a4f1caab1d | |||
| c8839f7658 | |||
| dfe3580607 | |||
| 1c02552e92 | |||
| ff7cbb97df | |||
| 09f3d3fb12 | |||
| 63defc25d2 | |||
| db39fc95f4 | |||
| 471dc714aa | |||
| fef665df73 | |||
| 7bfdf87bf0 | |||
| cf357d7058 | |||
| 618fa08aa5 | |||
| a40e7b4470 | |||
| f1894f6f9a | |||
| dfc2d452c5 | |||
| 66f23c3980 | |||
| 7a6ab31ad7 | |||
| 2f73dd5b59 | |||
| c658424c9f | |||
| bb58f2d162 | |||
| f54297f242 | |||
| b72d946062 | |||
| 883763c172 | |||
| 9063a5dbdc | |||
| 892e848985 | |||
| 0edb90bab2 | |||
| 8f71f8958e | |||
| fcb97cfd5e | |||
| 2983eb3113 | |||
| a968b1abc0 | |||
| 47c964d6fb | |||
| 22cb657ef1 | |||
| bb15d1e850 | |||
| 47680e43c5 | |||
| 0f1e44aac6 | |||
| 66aae91bca | |||
| 07bd76e219 | |||
| b6a7b3e9e4 | |||
| 1cf5cfce06 | |||
| 8ff90c4fc2 | |||
| 908c8eb42a | |||
| 0078293d4c | |||
| 9728dbeeac | |||
| 324029ca3b | |||
| 73be5b2ba1 | |||
| af904d23ac | |||
| ad84fc1479 | |||
| d5a8074b53 | |||
| c506fecc87 | |||
| d777810911 | |||
| bbdc07ee6c | |||
| 689338f059 | |||
| eee770514f | |||
| 5a0bda7ec4 | |||
| b454fd5d9e | |||
| 2a830ed498 | |||
| e98d1ec5a7 | |||
| 3ace97660f | |||
| 0824737757 | |||
| 8fdea033bc | |||
| 2e906fc5fa | |||
| a5a34934df | |||
| 08a8a0f29a | |||
| 519611c6c8 | |||
| a283c34dbb | |||
| f9fe86ee3e | |||
| 2e67152941 | |||
| 22bfec7259 | |||
| 1af9f9bd95 | |||
| 926451c8be | |||
| 7b3bef124d | |||
| 3be6e38af3 | |||
| f2290a43d9 | |||
| 4513663084 | |||
| 092cf6cfaf | |||
| 236f96e676 | |||
| 887ca971ab | |||
| 4cc195b681 | |||
| fc2be2b8d0 | |||
| 570bf1cb3c | |||
| 6ec9c72539 | |||
| 1a1086206c | |||
| f2766b103d | |||
| 62c9d44b04 | |||
| e394a7ff20 | |||
| 921ed63204 | |||
| 77dafb819f | |||
| 1da99f4003 | |||
| 120eaa6c56 | |||
| fb636ef98d | |||
| 6147a31b48 | |||
| 3f8707496f | |||
| de6caec685 | |||
| c8411e55d9 | |||
| d3aebdbec4 | |||
| a56e4ff436 | |||
| 9dcc9160b3 | |||
| 43df7fad46 | |||
| d2087a2cd9 | |||
| c681198179 | |||
| 105938df0b | |||
| 7b6fa12854 |
+36
-13
@@ -34,7 +34,7 @@ jobs:
|
||||
- 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
|
||||
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
- 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
|
||||
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 libdisplay-info-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
name: 'msrv - 1.72.0'
|
||||
name: 'msrv - 1.77.0'
|
||||
runs-on: ubuntu-22.04
|
||||
container: ubuntu:23.10
|
||||
|
||||
@@ -110,9 +110,9 @@ jobs:
|
||||
- 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
|
||||
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 libdisplay-info-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.72.0
|
||||
- uses: dtolnay/rust-toolchain@1.77.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
- 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
|
||||
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 libdisplay-info-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -153,11 +153,9 @@ jobs:
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set auto-self-update check-only
|
||||
rustup toolchain install nightly --profile minimal --component rustfmt
|
||||
rustup override set nightly
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
@@ -174,7 +172,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo dnf update -y
|
||||
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
|
||||
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 libdisplay-info-devel
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo build --all
|
||||
@@ -194,7 +192,7 @@ jobs:
|
||||
uses: DeterminateSystems/nix-installer-action@v3
|
||||
continue-on-error: true
|
||||
|
||||
- run: nix build
|
||||
- run: nix flake check
|
||||
continue-on-error: true
|
||||
|
||||
publish-wiki:
|
||||
@@ -209,3 +207,28 @@ jobs:
|
||||
lfs: true
|
||||
show-progress: false
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@86138cbd6328b21d759e89ab6e6dd6a139b22270
|
||||
|
||||
rustdoc:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Generate documentation
|
||||
run: cargo doc --no-deps -p niri-ipc
|
||||
|
||||
- run: cp ./resources/rustdoc-index.html ./target/doc/index.html
|
||||
|
||||
- name: Deploy documentation
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc
|
||||
force_orphan: true
|
||||
|
||||
Generated
+1119
-647
File diff suppressed because it is too large
Load Diff
+54
-32
@@ -2,22 +2,24 @@
|
||||
members = ["niri-visual-tests"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.7"
|
||||
version = "0.1.10-1"
|
||||
description = "A scrollable-tiling Wayland compositor"
|
||||
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/YaLTeR/niri"
|
||||
rust-version = "1.77"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.86"
|
||||
bitflags = "2.5.0"
|
||||
clap = { version = "~4.4.18", features = ["derive"] }
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
anyhow = "1.0.93"
|
||||
bitflags = "2.6.0"
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
k9 = "0.12.0"
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
tracy-client = { version = "0.17.0", default-features = false }
|
||||
tracy-client = { version = "0.17.4", default-features = false }
|
||||
|
||||
[workspace.dependencies.smithay]
|
||||
git = "https://github.com/Smithay/smithay.git"
|
||||
@@ -36,48 +38,53 @@ authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
readme = "README.md"
|
||||
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
arrayvec = "0.7.4"
|
||||
arrayvec = "0.7.6"
|
||||
async-channel = "2.3.1"
|
||||
async-io = { version = "1.13.0", optional = true }
|
||||
atomic = "0.6.0"
|
||||
bitflags.workspace = true
|
||||
bytemuck = { version = "1.16.1", features = ["derive"] }
|
||||
calloop = { version = "0.14.0", features = ["executor", "futures-io"] }
|
||||
bytemuck = { version = "1.19.0", features = ["derive"] }
|
||||
calloop = { version = "0.14.1", features = ["executor", "futures-io"] }
|
||||
clap = { workspace = true, features = ["string"] }
|
||||
directories = "5.0.1"
|
||||
drm-ffi = "0.8.0"
|
||||
fastrand = "2.1.0"
|
||||
futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] }
|
||||
drm-ffi = "0.9.0"
|
||||
fastrand = "2.2.0"
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
|
||||
git-version = "0.3.9"
|
||||
glam = "0.28.0"
|
||||
input = { version = "0.9.0", features = ["libinput_1_21"] }
|
||||
glam = "0.29.2"
|
||||
input = { version = "0.9.1", features = ["libinput_1_21"] }
|
||||
keyframe = { version = "1.1.1", default-features = false }
|
||||
libc = "0.2.155"
|
||||
log = { version = "0.4.21", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.7", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.7", path = "niri-ipc", features = ["clap"] }
|
||||
libc = "0.2.162"
|
||||
libdisplay-info = "0.1.0"
|
||||
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
niri-config = { version = "0.1.10-1", path = "niri-config" }
|
||||
niri-ipc = { version = "0.1.10-1", path = "niri-ipc", features = ["clap"] }
|
||||
notify-rust = { version = "~4.10.0", optional = true }
|
||||
ordered-float = "4.2.0"
|
||||
pangocairo = "0.19.8"
|
||||
pipewire = { version = "0.8.0", optional = true }
|
||||
png = "0.17.13"
|
||||
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.15"
|
||||
sd-notify = "0.4.1"
|
||||
ordered-float = "4.5.0"
|
||||
pango = { version = "0.20.4", features = ["v1_44"] }
|
||||
pangocairo = "0.20.4"
|
||||
pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] }
|
||||
png = "0.17.14"
|
||||
portable-atomic = { version = "1.9.0", default-features = false, features = ["float"] }
|
||||
profiling = "1.0.16"
|
||||
sd-notify = "0.4.3"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smithay-drm-extras.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
url = { version = "2.5.2", optional = true }
|
||||
xcursor = "0.3.5"
|
||||
url = { version = "2.5.3", optional = true }
|
||||
wayland-backend = "0.3.7"
|
||||
wayland-scanner = "0.31.5"
|
||||
xcursor = "0.3.8"
|
||||
zbus = { version = "~3.15.2", optional = true }
|
||||
|
||||
[dependencies.smithay]
|
||||
@@ -100,9 +107,9 @@ features = [
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
k9 = "0.12.0"
|
||||
proptest = "1.4.0"
|
||||
proptest-derive = "0.4.0"
|
||||
k9.workspace = true
|
||||
proptest = "1.5.0"
|
||||
proptest-derive = { version = "0.5.0", features = ["boxed_union"] }
|
||||
xshell = "0.2.6"
|
||||
|
||||
[features]
|
||||
@@ -115,6 +122,10 @@ systemd = ["dbus"]
|
||||
xdp-gnome-screencast = ["dbus", "pipewire"]
|
||||
# Enables the Tracy profiler instrumentation.
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]
|
||||
# Enables the on-demand Tracy profiler instrumentation.
|
||||
profile-with-tracy-ondemand = ["profile-with-tracy", "tracy-client/ondemand", "tracy-client/manual-lifetime"]
|
||||
# Enables Tracy allocation profiling.
|
||||
profile-with-tracy-allocations = ["profile-with-tracy"]
|
||||
# Enables dinit integration (global environment).
|
||||
dinit = []
|
||||
|
||||
@@ -128,7 +139,7 @@ lto = "thin"
|
||||
debug = false
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
version = "0.1.7"
|
||||
version = "0.1.10.1"
|
||||
assets = [
|
||||
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
|
||||
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
|
||||
@@ -140,3 +151,14 @@ assets = [
|
||||
[package.metadata.generate-rpm.requires]
|
||||
alacritty = "*"
|
||||
fuzzel = "*"
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "alacritty, fuzzel"
|
||||
assets = [
|
||||
["target/release/niri", "usr/bin/", "755"],
|
||||
["resources/niri-session", "usr/bin/", "755"],
|
||||
["resources/niri.desktop", "/usr/share/wayland-sessions/", "644"],
|
||||
["resources/niri-portals.conf", "/usr/share/xdg-desktop-portal/", "644"],
|
||||
["resources/niri.service", "/usr/lib/systemd/user/", "644"],
|
||||
["resources/niri-shutdown.target", "/usr/lib/systemd/user/", "644"],
|
||||
]
|
||||
|
||||
@@ -35,6 +35,7 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
|
||||
- You can [block out](https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules#block-out-from) sensitive windows from screencasts
|
||||
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
|
||||
- Configurable layout: gaps, borders, struts, window sizes
|
||||
- [Gradient borders](https://github.com/YaLTeR/niri/wiki/Configuration:-Layout#gradients) with Oklab and Oklch support
|
||||
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
|
||||
- Live-reloading config
|
||||
|
||||
@@ -48,8 +49,6 @@ A lot of the essential functionality is implemented, plus some goodies on top.
|
||||
Feel free to give niri a try: follow the instructions on the [Getting Started](https://github.com/YaLTeR/niri/wiki/Getting-Started) wiki page.
|
||||
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
|
||||
|
||||
Note that NVIDIA GPUs may have issues.
|
||||
|
||||
## Inspiration
|
||||
|
||||
Niri is heavily inspired by [PaperWM] which implements scrollable tiling on top of GNOME Shell.
|
||||
@@ -57,6 +56,16 @@ 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.
|
||||
Being a GNOME Shell extension, PaperWM has to work against Shell's global window coordinate space to prevent windows from overflowing.
|
||||
|
||||
## Tile Scrollably Elsewhere
|
||||
|
||||
Here are some other projects which implement a similar workflow:
|
||||
|
||||
- [PaperWM]: scrollable tiling on top of GNOME Shell.
|
||||
- [karousel]: scrollable tiling on top of KDE.
|
||||
- [papersway]: scrollable tiling on top of sway/i3.
|
||||
- [hyprscroller] and [hyprslidr]: scrollable tiling on top of Hyprland.
|
||||
- [PaperWM.spoon]: scrollable tiling on top of macOS.
|
||||
|
||||
## Contact
|
||||
|
||||
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org
|
||||
@@ -64,4 +73,8 @@ We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#
|
||||
[PaperWM]: https://github.com/paperwm/PaperWM
|
||||
[waybar]: https://github.com/Alexays/Waybar
|
||||
[fuzzel]: https://codeberg.org/dnkl/fuzzel
|
||||
|
||||
[karousel]: https://github.com/peterfajdiga/karousel
|
||||
[papersway]: https://spwhitton.name/tech/code/papersway/
|
||||
[hyprscroller]: https://github.com/dawsers/hyprscroller
|
||||
[hyprslidr]: https://gitlab.com/magus/hyprslidr
|
||||
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ignore-interior-mutability = [
|
||||
"smithay::desktop::Window",
|
||||
"smithay::output::Output",
|
||||
"wayland_server::backend::ClientId",
|
||||
]
|
||||
Generated
+21
-95
@@ -1,72 +1,12 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709610799,
|
||||
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "81c393c776d5379c030607866afef6406ca1be57",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709274179,
|
||||
"narHash": "sha256-O6EC6QELBLHzhdzBOJj0chx8AOcd4nDRECIagfT5Nd0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "4be608f4f81d351aacca01b21ffd91028c23cc22",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "monthly",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1705332318,
|
||||
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||
"lastModified": 1710156097,
|
||||
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,11 +17,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1709386671,
|
||||
"narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
|
||||
"lastModified": 1726365531,
|
||||
"narHash": "sha256-luAKNxWZ+ZN0kaHchx1OdLQ71n81Y31ryNPWP1YRDZc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fa9a51752f1b5de583ad5213eb621be071806663",
|
||||
"rev": "9299cdf978e15f448cf82667b0ffdd480b44ee48",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -93,42 +33,28 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709219524,
|
||||
"narHash": "sha256-8HHRXm4kYQLdUohNDUuCC3Rge7fXrtkjBUf0GERxrkM=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "9efa23c4dacee88b93540632eb3d88c5dfebfe17",
|
||||
"lastModified": 1727663505,
|
||||
"narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +1,251 @@
|
||||
# This flake file is community maintained
|
||||
# Maintainers:
|
||||
# Bill Sun (github/billksun)
|
||||
{
|
||||
description = "Niri: A scrollable-tiling Wayland compositor.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix/monthly";
|
||||
|
||||
# NOTE: This is not necessary for end users
|
||||
# You can omit it with `inputs.rust-overlay.follows = ""`
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
crane,
|
||||
nix-filter,
|
||||
flake-utils,
|
||||
fenix,
|
||||
...
|
||||
}: let
|
||||
systems = ["aarch64-linux" "x86_64-linux"];
|
||||
in
|
||||
flake-utils.lib.eachSystem systems (
|
||||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
toolchain = fenix.packages.${system}.complete.toolchain;
|
||||
craneLib = crane.lib.${system}.overrideToolchain toolchain;
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-filter,
|
||||
rust-overlay,
|
||||
}:
|
||||
let
|
||||
niri-package =
|
||||
{
|
||||
lib,
|
||||
cairo,
|
||||
clang,
|
||||
dbus,
|
||||
libGL,
|
||||
libclang,
|
||||
libdisplay-info,
|
||||
libinput,
|
||||
seatd,
|
||||
libxkbcommon,
|
||||
mesa,
|
||||
pango,
|
||||
pipewire,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
systemd,
|
||||
wayland,
|
||||
withDbus ? true,
|
||||
withSystemd ? true,
|
||||
withScreencastSupport ? true,
|
||||
withDinit ? false,
|
||||
}:
|
||||
|
||||
craneArgs = {
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "niri";
|
||||
version = self.rev or "dirty";
|
||||
version = self.shortRev or self.dirtyShortRev or "unknown";
|
||||
|
||||
src = nixpkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = path: type:
|
||||
(builtins.match "resources" path == null) ||
|
||||
((craneLib.filterCargoSources path type) &&
|
||||
(builtins.match "niri-visual-tests" path == null));
|
||||
src = nix-filter.lib.filter {
|
||||
root = self;
|
||||
include = [
|
||||
"niri-config"
|
||||
"niri-ipc"
|
||||
"niri-visual-tests"
|
||||
"resources"
|
||||
"src"
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
];
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
autoPatchelfHook
|
||||
postPatch = ''
|
||||
patchShebangs resources/niri-session
|
||||
substituteInPlace resources/niri.service \
|
||||
--replace-fail '/usr/bin' "$out/bin"
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
# NOTE: This is only used for Git dependencies
|
||||
allowBuiltinFetchGit = true;
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
clang
|
||||
gdk-pixbuf
|
||||
graphene
|
||||
gtk4
|
||||
libadwaita
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
wayland
|
||||
systemd # For libudev
|
||||
seatd # For libseat
|
||||
libxkbcommon
|
||||
libinput
|
||||
mesa # For libgbm
|
||||
fontconfig
|
||||
stdenv.cc.cc.lib
|
||||
pipewire
|
||||
pango
|
||||
];
|
||||
buildInputs =
|
||||
[
|
||||
cairo
|
||||
dbus
|
||||
libGL
|
||||
libdisplay-info
|
||||
libinput
|
||||
seatd
|
||||
libxkbcommon
|
||||
mesa # libgbm
|
||||
pango
|
||||
wayland
|
||||
]
|
||||
++ lib.optional (withDbus || withScreencastSupport || withSystemd) dbus
|
||||
++ lib.optional withScreencastSupport pipewire
|
||||
# Also includes libudev
|
||||
++ lib.optional withSystemd systemd;
|
||||
|
||||
runtimeDependencies = with pkgs; [
|
||||
wayland
|
||||
mesa
|
||||
libglvnd # For libEGL
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
];
|
||||
buildFeatures =
|
||||
lib.optional withDbus "dbus"
|
||||
++ lib.optional withDinit "dinit"
|
||||
++ lib.optional withScreencastSupport "xdp-gnome-screencast"
|
||||
++ lib.optional withSystemd "systemd";
|
||||
buildNoDefaultFeatures = true;
|
||||
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
postInstall =
|
||||
''
|
||||
install -Dm644 resources/niri.desktop -t $out/share/wayland-sessions
|
||||
install -Dm644 resources/niri-portals.conf -t $out/share/xdg-desktop-portal
|
||||
''
|
||||
+ lib.optionalString withSystemd ''
|
||||
install -Dm755 resources/niri-session $out/bin/niri-session
|
||||
install -Dm644 resources/niri{.service,-shutdown.target} -t $out/share/systemd/user
|
||||
'';
|
||||
|
||||
env = {
|
||||
LIBCLANG_PATH = lib.getLib libclang + "/lib";
|
||||
|
||||
# Force linking with libEGL and libwayland-client
|
||||
# so they can be discovered by `dlopen()`
|
||||
RUSTFLAGS = toString (
|
||||
map (arg: "-C link-arg=" + arg) [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
"-lEGL"
|
||||
"-lwayland-client"
|
||||
"-Wl,--pop-state"
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
passthru = {
|
||||
providedSessions = [ "niri" ];
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Scrollable-tiling Wayland compositor";
|
||||
homepage = "https://github.com/YaLTeR/niri";
|
||||
license = lib.licenses.gpl3Only;
|
||||
mainProgram = "niri";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly craneArgs;
|
||||
niri = craneLib.buildPackage (craneArgs // {inherit cargoArtifacts;});
|
||||
in {
|
||||
formatter = pkgs.alejandra;
|
||||
inherit (nixpkgs) lib;
|
||||
# Support all Linux systems that the nixpkgs flake exposes
|
||||
systems = lib.intersectLists lib.systems.flakeExposed lib.platforms.linux;
|
||||
|
||||
checks.niri = niri;
|
||||
packages.default = niri;
|
||||
forAllSystems = lib.genAttrs systems;
|
||||
nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
|
||||
in
|
||||
{
|
||||
checks = forAllSystems (system: {
|
||||
# We use the debug build here to save a bit of time
|
||||
inherit (self.packages.${system}) niri-debug;
|
||||
});
|
||||
|
||||
devShells.default = pkgs.mkShell.override {stdenv = pkgs.clangStdenv;} {
|
||||
inherit (niri) nativeBuildInputs buildInputs LIBCLANG_PATH;
|
||||
packages = niri.runtimeDependencies;
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system};
|
||||
rust-bin = rust-overlay.lib.mkRustBin { } pkgs;
|
||||
inherit (self.packages.${system}) niri;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
# We don't use the toolchain from nixpkgs
|
||||
# because we prefer a nightly toolchain
|
||||
# and we *require* a nightly rustfmt
|
||||
(rust-bin.selectLatestNightlyWith (
|
||||
toolchain:
|
||||
toolchain.default.override {
|
||||
extensions = [
|
||||
# includes already:
|
||||
# rustc
|
||||
# cargo
|
||||
# rust-std
|
||||
# rust-docs
|
||||
# rustfmt-preview
|
||||
# clippy-preview
|
||||
"rust-analyzer"
|
||||
"rust-src"
|
||||
];
|
||||
}
|
||||
))
|
||||
];
|
||||
|
||||
# Force linking to libEGL, which is always dlopen()ed, and to
|
||||
# libwayland-client, which is always dlopen()ed except by the
|
||||
# obscure winit backend.
|
||||
RUSTFLAGS = map (a: "-C link-arg=${a}") [
|
||||
"-Wl,--push-state,--no-as-needed"
|
||||
"-lEGL"
|
||||
"-lwayland-client"
|
||||
"-Wl,--pop-state"
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
nativeBuildInputs = [
|
||||
pkgs.clang
|
||||
pkgs.pkg-config
|
||||
pkgs.wrapGAppsHook4 # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
buildInputs = niri.buildInputs ++ [
|
||||
pkgs.libadwaita # For `niri-visual-tests`
|
||||
];
|
||||
|
||||
env = {
|
||||
inherit (niri) LIBCLANG_PATH;
|
||||
|
||||
# WARN: Do not overwrite this variable in your shell!
|
||||
# It is required for `dlopen()` to work on some libraries; see the comment
|
||||
# in the package expression
|
||||
#
|
||||
# This should only be set with `CARGO_BUILD_RUSTFLAGS="$CARGO_BUILD_RUSTFLAGS -C your-flags"`
|
||||
CARGO_BUILD_RUSTFLAGS = niri.RUSTFLAGS;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
let
|
||||
niri = nixpkgsFor.${system}.callPackage niri-package { };
|
||||
in
|
||||
{
|
||||
inherit niri;
|
||||
|
||||
# NOTE: This is for development purposes only
|
||||
#
|
||||
# It is primarily to help with quickly iterating on
|
||||
# changes made to the above expression - though it is
|
||||
# also not stripped in order to better debug niri itself
|
||||
niri-debug = niri.overrideAttrs (
|
||||
newAttrs: oldAttrs: {
|
||||
pname = oldAttrs.pname + "-debug";
|
||||
|
||||
cargoBuildType = "debug";
|
||||
cargoCheckType = newAttrs.cargoBuildType;
|
||||
|
||||
dontStrip = true;
|
||||
}
|
||||
);
|
||||
|
||||
default = niri;
|
||||
}
|
||||
);
|
||||
|
||||
overlays.default = final: _: {
|
||||
niri = final.callPackage niri-package { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
csscolorparser = "0.6.2"
|
||||
csscolorparser = "0.7.0"
|
||||
knuffel = "3.2.0"
|
||||
miette = "5.10.0"
|
||||
niri-ipc = { version = "0.1.7", path = "../niri-ipc" }
|
||||
regex = "1.10.5"
|
||||
niri-ipc = { version = "0.1.10-1", path = "../niri-ipc" }
|
||||
regex = "1.11.1"
|
||||
smithay = { workspace = true, features = ["backend_libinput"] }
|
||||
tracing.workspace = true
|
||||
tracy-client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
k9.workspace = true
|
||||
miette = { version = "5.10.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
+988
-149
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct KdlCodeBlock {
|
||||
filename: String,
|
||||
code: String,
|
||||
line_number: usize,
|
||||
must_fail: bool,
|
||||
}
|
||||
|
||||
fn extract_kdl_from_file(file_contents: &str, filename: &str) -> Vec<KdlCodeBlock> {
|
||||
let mut lines = file_contents
|
||||
.lines()
|
||||
.map(|line| {
|
||||
// Removes the > from callouts that might contain ```kdl```
|
||||
let line = line.trim();
|
||||
if line.starts_with('>') {
|
||||
if line.len() == 1 {
|
||||
""
|
||||
} else {
|
||||
&line[2..]
|
||||
}
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.enumerate();
|
||||
|
||||
let mut kdl_code_blocks = vec![];
|
||||
|
||||
while let Some((line_number, line)) = lines.next() {
|
||||
if !line.starts_with("```kdl") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut snippet = String::new();
|
||||
|
||||
for (_, line) in lines
|
||||
.by_ref()
|
||||
.take_while(|(_, line)| !line.starts_with("```"))
|
||||
{
|
||||
snippet.push_str(line);
|
||||
snippet.push('\n');
|
||||
}
|
||||
|
||||
kdl_code_blocks.push(KdlCodeBlock {
|
||||
code: snippet,
|
||||
line_number,
|
||||
filename: filename.to_string(),
|
||||
must_fail: line.contains("must-fail"),
|
||||
});
|
||||
}
|
||||
|
||||
kdl_code_blocks
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiki_docs_parses() {
|
||||
let wiki_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki");
|
||||
|
||||
let code_blocks = fs::read_dir(wiki_dir)
|
||||
.unwrap()
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.file_type().is_ok_and(|ft| ft.is_file()))
|
||||
.filter(|file| {
|
||||
file.path()
|
||||
.extension()
|
||||
.map(|ext| ext == "md")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.flat_map(|file| {
|
||||
let file_contents = fs::read_to_string(file.path()).unwrap();
|
||||
let file_path = file.path();
|
||||
let filename = file_path.to_str().unwrap();
|
||||
extract_kdl_from_file(&file_contents, filename)
|
||||
});
|
||||
|
||||
let mut errors = vec![];
|
||||
|
||||
for KdlCodeBlock {
|
||||
code,
|
||||
line_number,
|
||||
filename,
|
||||
must_fail,
|
||||
} in code_blocks
|
||||
{
|
||||
if let Err(error) = niri_config::Config::parse(&filename, &code) {
|
||||
if !must_fail {
|
||||
errors.push(format!(
|
||||
"Error parsing wiki KDL code block at {}:{}: {:?}",
|
||||
filename,
|
||||
line_number,
|
||||
miette::Report::new(error)
|
||||
));
|
||||
}
|
||||
} else if must_fail {
|
||||
errors.push(format!(
|
||||
"Expected error parsing wiki KDL code block at {}:{}",
|
||||
filename, line_number
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
panic!(
|
||||
"Errors parsing {} wiki KDL code blocks:\n{}",
|
||||
errors.len(),
|
||||
errors.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -1,16 +1,22 @@
|
||||
[package]
|
||||
name = "niri-ipc"
|
||||
version.workspace = true
|
||||
description.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
description = "Types and helpers for interfacing with the niri Wayland compositor."
|
||||
keywords = ["wayland"]
|
||||
categories = ["api-bindings", "os"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, optional = true }
|
||||
schemars = { version = "0.8.21", optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
clap = ["dep:clap"]
|
||||
json-schema = ["dep:schemars"]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# niri-ipc
|
||||
|
||||
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
|
||||
|
||||
## Backwards compatibility
|
||||
|
||||
This crate follows the niri version.
|
||||
It is **not** API-stable in terms of the Rust semver.
|
||||
In particular, expect new struct fields and enum variants to be added in patch version bumps.
|
||||
|
||||
Use an exact version requirement to avoid breaking changes:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
niri-ipc = "=0.1.10"
|
||||
```
|
||||
+409
-101
@@ -1,4 +1,38 @@
|
||||
//! Types for communicating with niri via IPC.
|
||||
//!
|
||||
//! After connecting to the niri socket, you can send a single [`Request`] and receive a single
|
||||
//! [`Reply`], which is a `Result` wrapping a [`Response`]. If you requested an event stream, you
|
||||
//! can keep reading [`Event`]s from the socket after the response.
|
||||
//!
|
||||
//! You can use the [`socket::Socket`] helper if you're fine with blocking communication. However,
|
||||
//! it is a fairly simple helper, so if you need async, or if you're using a different language,
|
||||
//! you are encouraged to communicate with the socket manually.
|
||||
//!
|
||||
//! 1. Read the socket filesystem path from [`socket::SOCKET_PATH_ENV`] (`$NIRI_SOCKET`).
|
||||
//! 2. Connect to the socket and write a JSON-formatted [`Request`] on a single line. You can follow
|
||||
//! up with a line break and a flush, or just flush and shutdown the write end of the socket.
|
||||
//! 3. Niri will respond with a single line JSON-formatted [`Reply`].
|
||||
//! 4. If you requested an event stream, niri will keep responding with JSON-formatted [`Event`]s,
|
||||
//! on a single line each.
|
||||
//!
|
||||
//! ## Backwards compatibility
|
||||
//!
|
||||
//! This crate follows the niri version. It is **not** API-stable in terms of the Rust semver. In
|
||||
//! particular, expect new struct fields and enum variants to be added in patch version bumps.
|
||||
//!
|
||||
//! Use an exact version requirement to avoid breaking changes:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! niri-ipc = "=0.1.10"
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! This crate defines the following features:
|
||||
//! - `json-schema`: derives the [schemars](https://lib.rs/crates/schemars) `JsonSchema` trait for
|
||||
//! the types.
|
||||
//! - `clap`: derives the clap CLI parsing traits for some types. Used internally by niri itself.
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -6,16 +40,25 @@ use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod socket;
|
||||
pub use socket::{Socket, SOCKET_PATH_ENV};
|
||||
pub mod socket;
|
||||
pub mod state;
|
||||
|
||||
/// Request from client to niri.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum Request {
|
||||
/// Request the version string for the running niri instance.
|
||||
Version,
|
||||
/// Request information about connected outputs.
|
||||
Outputs,
|
||||
/// Request information about workspaces.
|
||||
Workspaces,
|
||||
/// Request information about open windows.
|
||||
Windows,
|
||||
/// Request information about the configured keyboard layouts.
|
||||
KeyboardLayouts,
|
||||
/// Request information about the focused output.
|
||||
FocusedOutput,
|
||||
/// Request information about the focused window.
|
||||
FocusedWindow,
|
||||
/// Perform an action.
|
||||
@@ -31,10 +74,21 @@ pub enum Request {
|
||||
/// Configuration to apply.
|
||||
action: OutputAction,
|
||||
},
|
||||
/// Request information about workspaces.
|
||||
Workspaces,
|
||||
/// Request information about the focused output.
|
||||
FocusedOutput,
|
||||
/// Start continuously receiving events from the compositor.
|
||||
///
|
||||
/// The compositor should reply with `Reply::Ok(Response::Handled)`, then continuously send
|
||||
/// [`Event`]s, one per line.
|
||||
///
|
||||
/// The event stream will always give you the full current state up-front. For example, the
|
||||
/// first workspace-related event you will receive will be [`Event::WorkspacesChanged`]
|
||||
/// containing the full current workspaces state. You *do not* need to separately send
|
||||
/// [`Request::Workspaces`] when using the event stream.
|
||||
///
|
||||
/// Where reasonable, event stream state updates are atomic, though this is not always the
|
||||
/// case. For example, a window may end up with a workspace id for a workspace that had already
|
||||
/// been removed. This can happen if the corresponding [`Event::WorkspacesChanged`] arrives
|
||||
/// before the corresponding [`Event::WindowOpenedOrChanged`].
|
||||
EventStream,
|
||||
/// Respond with an error (for testing error handling).
|
||||
ReturnError,
|
||||
}
|
||||
@@ -51,6 +105,7 @@ pub type Reply = Result<Response, String>;
|
||||
|
||||
/// Successful response from niri to client.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum Response {
|
||||
/// A request that does not need a response was handled successfully.
|
||||
Handled,
|
||||
@@ -58,16 +113,20 @@ pub enum Response {
|
||||
Version(String),
|
||||
/// Information about connected outputs.
|
||||
///
|
||||
/// Map from connector name to output info.
|
||||
/// Map from output name to output info.
|
||||
Outputs(HashMap<String, Output>),
|
||||
/// Information about workspaces.
|
||||
Workspaces(Vec<Workspace>),
|
||||
/// Information about open windows.
|
||||
Windows(Vec<Window>),
|
||||
/// Information about the keyboard layout.
|
||||
KeyboardLayouts(KeyboardLayouts),
|
||||
/// Information about the focused output.
|
||||
FocusedOutput(Option<Output>),
|
||||
/// Information about the focused window.
|
||||
FocusedWindow(Option<Window>),
|
||||
/// Output configuration change result.
|
||||
OutputConfigChanged(OutputConfigChanged),
|
||||
/// Information about workspaces.
|
||||
Workspaces(Vec<Workspace>),
|
||||
/// Information about the focused output.
|
||||
FocusedOutput(Option<Output>),
|
||||
}
|
||||
|
||||
/// Actions that niri can perform.
|
||||
@@ -77,6 +136,7 @@ pub enum Response {
|
||||
#[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"))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum Action {
|
||||
/// Exit niri.
|
||||
Quit {
|
||||
@@ -85,7 +145,9 @@ pub enum Action {
|
||||
skip_confirmation: bool,
|
||||
},
|
||||
/// Power off all monitors via DPMS.
|
||||
PowerOffMonitors,
|
||||
PowerOffMonitors {},
|
||||
/// Power on all monitors via DPMS.
|
||||
PowerOnMonitors {},
|
||||
/// Spawn a command.
|
||||
Spawn {
|
||||
/// Command to spawn.
|
||||
@@ -99,77 +161,135 @@ pub enum Action {
|
||||
delay_ms: Option<u16>,
|
||||
},
|
||||
/// Open the screenshot UI.
|
||||
Screenshot,
|
||||
Screenshot {},
|
||||
/// Screenshot the focused screen.
|
||||
ScreenshotScreen,
|
||||
/// Screenshot the focused window.
|
||||
ScreenshotWindow,
|
||||
/// Close the focused window.
|
||||
CloseWindow,
|
||||
/// Toggle fullscreen on the focused window.
|
||||
FullscreenWindow,
|
||||
ScreenshotScreen {},
|
||||
/// Screenshot a window.
|
||||
#[cfg_attr(feature = "clap", clap(about = "Screenshot the focused window"))]
|
||||
ScreenshotWindow {
|
||||
/// Id of the window to screenshot.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Close a window.
|
||||
#[cfg_attr(feature = "clap", clap(about = "Close the focused window"))]
|
||||
CloseWindow {
|
||||
/// Id of the window to close.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Toggle fullscreen on a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Toggle fullscreen on the focused window")
|
||||
)]
|
||||
FullscreenWindow {
|
||||
/// Id of the window to toggle fullscreen of.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Focus a window by id.
|
||||
FocusWindow {
|
||||
/// Id of the window to focus.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: u64,
|
||||
},
|
||||
/// Focus the column to the left.
|
||||
FocusColumnLeft,
|
||||
FocusColumnLeft {},
|
||||
/// Focus the column to the right.
|
||||
FocusColumnRight,
|
||||
FocusColumnRight {},
|
||||
/// Focus the first column.
|
||||
FocusColumnFirst,
|
||||
FocusColumnFirst {},
|
||||
/// Focus the last column.
|
||||
FocusColumnLast,
|
||||
FocusColumnLast {},
|
||||
/// Focus the next column to the right, looping if at end.
|
||||
FocusColumnRightOrFirst,
|
||||
FocusColumnRightOrFirst {},
|
||||
/// Focus the next column to the left, looping if at start.
|
||||
FocusColumnLeftOrLast,
|
||||
FocusColumnLeftOrLast {},
|
||||
/// Focus the window or the monitor above.
|
||||
FocusWindowOrMonitorUp {},
|
||||
/// Focus the window or the monitor below.
|
||||
FocusWindowOrMonitorDown {},
|
||||
/// Focus the column or the monitor to the left.
|
||||
FocusColumnOrMonitorLeft,
|
||||
FocusColumnOrMonitorLeft {},
|
||||
/// Focus the column or the monitor to the right.
|
||||
FocusColumnOrMonitorRight,
|
||||
FocusColumnOrMonitorRight {},
|
||||
/// Focus the window below.
|
||||
FocusWindowDown,
|
||||
FocusWindowDown {},
|
||||
/// Focus the window above.
|
||||
FocusWindowUp,
|
||||
FocusWindowUp {},
|
||||
/// Focus the window below or the column to the left.
|
||||
FocusWindowDownOrColumnLeft,
|
||||
FocusWindowDownOrColumnLeft {},
|
||||
/// Focus the window below or the column to the right.
|
||||
FocusWindowDownOrColumnRight,
|
||||
FocusWindowDownOrColumnRight {},
|
||||
/// Focus the window above or the column to the left.
|
||||
FocusWindowUpOrColumnLeft,
|
||||
FocusWindowUpOrColumnLeft {},
|
||||
/// Focus the window above or the column to the right.
|
||||
FocusWindowUpOrColumnRight,
|
||||
FocusWindowUpOrColumnRight {},
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceDown,
|
||||
FocusWindowOrWorkspaceDown {},
|
||||
/// Focus the window or the workspace above.
|
||||
FocusWindowOrWorkspaceUp,
|
||||
FocusWindowOrWorkspaceUp {},
|
||||
/// Move the focused column to the left.
|
||||
MoveColumnLeft,
|
||||
MoveColumnLeft {},
|
||||
/// Move the focused column to the right.
|
||||
MoveColumnRight,
|
||||
MoveColumnRight {},
|
||||
/// Move the focused column to the start of the workspace.
|
||||
MoveColumnToFirst,
|
||||
MoveColumnToFirst {},
|
||||
/// Move the focused column to the end of the workspace.
|
||||
MoveColumnToLast,
|
||||
MoveColumnToLast {},
|
||||
/// Move the focused column to the left or to the monitor to the left.
|
||||
MoveColumnLeftOrToMonitorLeft {},
|
||||
/// Move the focused column to the right or to the monitor to the right.
|
||||
MoveColumnRightOrToMonitorRight {},
|
||||
/// Move the focused window down in a column.
|
||||
MoveWindowDown,
|
||||
MoveWindowDown {},
|
||||
/// Move the focused window up in a column.
|
||||
MoveWindowUp,
|
||||
MoveWindowUp {},
|
||||
/// Move the focused window down in a column or to the workspace below.
|
||||
MoveWindowDownOrToWorkspaceDown,
|
||||
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,
|
||||
MoveWindowUpOrToWorkspaceUp {},
|
||||
/// Consume or expel a window left.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Consume or expel the focused window left")
|
||||
)]
|
||||
ConsumeOrExpelWindowLeft {
|
||||
/// Id of the window to consume or expel.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Consume or expel a window right.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Consume or expel the focused window right")
|
||||
)]
|
||||
ConsumeOrExpelWindowRight {
|
||||
/// Id of the window to consume or expel.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Consume the window to the right into the focused column.
|
||||
ConsumeWindowIntoColumn,
|
||||
ConsumeWindowIntoColumn {},
|
||||
/// Expel the focused window from the column.
|
||||
ExpelWindowFromColumn,
|
||||
ExpelWindowFromColumn {},
|
||||
/// Center the focused column on the screen.
|
||||
CenterColumn,
|
||||
CenterColumn {},
|
||||
/// Focus the workspace below.
|
||||
FocusWorkspaceDown,
|
||||
FocusWorkspaceDown {},
|
||||
/// Focus the workspace above.
|
||||
FocusWorkspaceUp,
|
||||
FocusWorkspaceUp {},
|
||||
/// Focus a workspace by reference (index or name).
|
||||
FocusWorkspace {
|
||||
/// Reference (index or name) of the workspace to focus.
|
||||
@@ -177,21 +297,31 @@ pub enum Action {
|
||||
reference: WorkspaceReferenceArg,
|
||||
},
|
||||
/// Focus the previous workspace.
|
||||
FocusWorkspacePrevious,
|
||||
FocusWorkspacePrevious {},
|
||||
/// Move the focused window to the workspace below.
|
||||
MoveWindowToWorkspaceDown,
|
||||
MoveWindowToWorkspaceDown {},
|
||||
/// Move the focused window to the workspace above.
|
||||
MoveWindowToWorkspaceUp,
|
||||
/// Move the focused window to a workspace by reference (index or name).
|
||||
MoveWindowToWorkspaceUp {},
|
||||
/// Move a window to a workspace.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Move the focused window to a workspace by reference (index or name)")
|
||||
)]
|
||||
MoveWindowToWorkspace {
|
||||
/// Id of the window to move.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
window_id: Option<u64>,
|
||||
|
||||
/// Reference (index or name) of the workspace to move the window to.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
reference: WorkspaceReferenceArg,
|
||||
},
|
||||
/// Move the focused column to the workspace below.
|
||||
MoveColumnToWorkspaceDown,
|
||||
MoveColumnToWorkspaceDown {},
|
||||
/// Move the focused column to the workspace above.
|
||||
MoveColumnToWorkspaceUp,
|
||||
MoveColumnToWorkspaceUp {},
|
||||
/// Move the focused column to a workspace by reference (index or name).
|
||||
MoveColumnToWorkspace {
|
||||
/// Reference (index or name) of the workspace to move the column to.
|
||||
@@ -199,45 +329,73 @@ pub enum Action {
|
||||
reference: WorkspaceReferenceArg,
|
||||
},
|
||||
/// Move the focused workspace down.
|
||||
MoveWorkspaceDown,
|
||||
MoveWorkspaceDown {},
|
||||
/// Move the focused workspace up.
|
||||
MoveWorkspaceUp,
|
||||
MoveWorkspaceUp {},
|
||||
/// Focus the monitor to the left.
|
||||
FocusMonitorLeft,
|
||||
FocusMonitorLeft {},
|
||||
/// Focus the monitor to the right.
|
||||
FocusMonitorRight,
|
||||
FocusMonitorRight {},
|
||||
/// Focus the monitor below.
|
||||
FocusMonitorDown,
|
||||
FocusMonitorDown {},
|
||||
/// Focus the monitor above.
|
||||
FocusMonitorUp,
|
||||
FocusMonitorUp {},
|
||||
/// Move the focused window to the monitor to the left.
|
||||
MoveWindowToMonitorLeft,
|
||||
MoveWindowToMonitorLeft {},
|
||||
/// Move the focused window to the monitor to the right.
|
||||
MoveWindowToMonitorRight,
|
||||
MoveWindowToMonitorRight {},
|
||||
/// Move the focused window to the monitor below.
|
||||
MoveWindowToMonitorDown,
|
||||
MoveWindowToMonitorDown {},
|
||||
/// Move the focused window to the monitor above.
|
||||
MoveWindowToMonitorUp,
|
||||
MoveWindowToMonitorUp {},
|
||||
/// Move the focused column to the monitor to the left.
|
||||
MoveColumnToMonitorLeft,
|
||||
MoveColumnToMonitorLeft {},
|
||||
/// Move the focused column to the monitor to the right.
|
||||
MoveColumnToMonitorRight,
|
||||
MoveColumnToMonitorRight {},
|
||||
/// Move the focused column to the monitor below.
|
||||
MoveColumnToMonitorDown,
|
||||
MoveColumnToMonitorDown {},
|
||||
/// Move the focused column to the monitor above.
|
||||
MoveColumnToMonitorUp,
|
||||
/// Change the height of the focused window.
|
||||
MoveColumnToMonitorUp {},
|
||||
/// Change the height of a window.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Change the height of the focused window")
|
||||
)]
|
||||
SetWindowHeight {
|
||||
/// Id of the window whose height to set.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
|
||||
/// How to change the height.
|
||||
#[cfg_attr(feature = "clap", arg())]
|
||||
change: SizeChange,
|
||||
},
|
||||
/// Reset the height of the focused window back to automatic.
|
||||
ResetWindowHeight,
|
||||
/// Reset the height of a window back to automatic.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
clap(about = "Reset the height of the focused window back to automatic")
|
||||
)]
|
||||
ResetWindowHeight {
|
||||
/// Id of the window whose height to reset.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Switch between preset column widths.
|
||||
SwitchPresetColumnWidth,
|
||||
SwitchPresetColumnWidth {},
|
||||
/// Switch between preset window heights.
|
||||
SwitchPresetWindowHeight {
|
||||
/// Id of the window whose height to switch.
|
||||
///
|
||||
/// If `None`, uses the focused window.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// Toggle the maximized state of the focused column.
|
||||
MaximizeColumn,
|
||||
MaximizeColumn {},
|
||||
/// Change the width of the focused column.
|
||||
SetColumnWidth {
|
||||
/// How to change the width.
|
||||
@@ -251,25 +409,26 @@ pub enum Action {
|
||||
layout: LayoutSwitchTarget,
|
||||
},
|
||||
/// Show the hotkey overlay.
|
||||
ShowHotkeyOverlay,
|
||||
ShowHotkeyOverlay {},
|
||||
/// Move the focused workspace to the monitor to the left.
|
||||
MoveWorkspaceToMonitorLeft,
|
||||
MoveWorkspaceToMonitorLeft {},
|
||||
/// Move the focused workspace to the monitor to the right.
|
||||
MoveWorkspaceToMonitorRight,
|
||||
MoveWorkspaceToMonitorRight {},
|
||||
/// Move the focused workspace to the monitor below.
|
||||
MoveWorkspaceToMonitorDown,
|
||||
MoveWorkspaceToMonitorDown {},
|
||||
/// Move the focused workspace to the monitor above.
|
||||
MoveWorkspaceToMonitorUp,
|
||||
MoveWorkspaceToMonitorUp {},
|
||||
/// Toggle a debug tint on windows.
|
||||
ToggleDebugTint,
|
||||
ToggleDebugTint {},
|
||||
/// Toggle visualization of render element opaque regions.
|
||||
DebugToggleOpaqueRegions,
|
||||
DebugToggleOpaqueRegions {},
|
||||
/// Toggle visualization of output damage.
|
||||
DebugToggleDamage,
|
||||
DebugToggleDamage {},
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum SizeChange {
|
||||
/// Set the size in logical pixels.
|
||||
SetFixed(i32),
|
||||
@@ -281,9 +440,12 @@ pub enum SizeChange {
|
||||
AdjustProportion(f64),
|
||||
}
|
||||
|
||||
/// Workspace reference (index or name) to operate on.
|
||||
/// Workspace reference (id, index or name) to operate on.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum WorkspaceReferenceArg {
|
||||
/// Id of the workspace.
|
||||
Id(u64),
|
||||
/// Index of the workspace.
|
||||
Index(u8),
|
||||
/// Name of the workspace.
|
||||
@@ -292,6 +454,7 @@ pub enum WorkspaceReferenceArg {
|
||||
|
||||
/// Layout to switch to.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum LayoutSwitchTarget {
|
||||
/// The next configured layout.
|
||||
Next,
|
||||
@@ -306,6 +469,7 @@ pub enum LayoutSwitchTarget {
|
||||
#[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"))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum OutputAction {
|
||||
/// Turn off the output.
|
||||
Off,
|
||||
@@ -337,23 +501,17 @@ pub enum OutputAction {
|
||||
#[cfg_attr(feature = "clap", command(subcommand))]
|
||||
position: PositionToSet,
|
||||
},
|
||||
/// Toggle variable refresh rate.
|
||||
/// Set the variable refresh rate mode.
|
||||
Vrr {
|
||||
/// Whether to enable variable refresh rate.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
arg(
|
||||
value_name = "ON|OFF",
|
||||
action = clap::ArgAction::Set,
|
||||
value_parser = clap::builder::BoolishValueParser::new(),
|
||||
),
|
||||
)]
|
||||
enable: bool,
|
||||
/// Variable refresh rate mode to set.
|
||||
#[cfg_attr(feature = "clap", command(flatten))]
|
||||
vrr: VrrToSet,
|
||||
},
|
||||
}
|
||||
|
||||
/// Output mode to set.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum ModeToSet {
|
||||
/// Niri will pick the mode automatically.
|
||||
Automatic,
|
||||
@@ -363,6 +521,7 @@ pub enum ModeToSet {
|
||||
|
||||
/// Output mode as set in the config file.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct ConfiguredMode {
|
||||
/// Width in physical pixels.
|
||||
pub width: u16,
|
||||
@@ -374,6 +533,7 @@ pub struct ConfiguredMode {
|
||||
|
||||
/// Output scale to set.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum ScaleToSet {
|
||||
/// Niri will pick the scale automatically.
|
||||
Automatic,
|
||||
@@ -386,6 +546,7 @@ pub enum ScaleToSet {
|
||||
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
|
||||
#[cfg_attr(feature = "clap", command(subcommand_value_name = "POSITION"))]
|
||||
#[cfg_attr(feature = "clap", command(subcommand_help_heading = "Position Values"))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum PositionToSet {
|
||||
/// Position the output automatically.
|
||||
#[cfg_attr(feature = "clap", command(name = "auto"))]
|
||||
@@ -398,6 +559,7 @@ pub enum PositionToSet {
|
||||
/// Output position as set in the config file.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::Args))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct ConfiguredPosition {
|
||||
/// Logical X position.
|
||||
pub x: i32,
|
||||
@@ -405,8 +567,30 @@ pub struct ConfiguredPosition {
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
/// Output VRR to set.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::Args))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct VrrToSet {
|
||||
/// Whether to enable variable refresh rate.
|
||||
#[cfg_attr(
|
||||
feature = "clap",
|
||||
arg(
|
||||
value_name = "ON|OFF",
|
||||
action = clap::ArgAction::Set,
|
||||
value_parser = clap::builder::BoolishValueParser::new(),
|
||||
hide_possible_values = true,
|
||||
),
|
||||
)]
|
||||
pub vrr: bool,
|
||||
/// Only enable when the output shows a window matching the variable-refresh-rate window rule.
|
||||
#[cfg_attr(feature = "clap", arg(long))]
|
||||
pub on_demand: bool,
|
||||
}
|
||||
|
||||
/// Connected output.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Output {
|
||||
/// Name of the output.
|
||||
pub name: String,
|
||||
@@ -414,6 +598,8 @@ pub struct Output {
|
||||
pub make: String,
|
||||
/// Textual description of the model.
|
||||
pub model: String,
|
||||
/// Serial of the output, if known.
|
||||
pub serial: Option<String>,
|
||||
/// Physical width and height of the output in millimeters, if known.
|
||||
pub physical_size: Option<(u32, u32)>,
|
||||
/// Available modes for the output.
|
||||
@@ -433,7 +619,8 @@ pub struct Output {
|
||||
}
|
||||
|
||||
/// Output mode.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Mode {
|
||||
/// Width in physical pixels.
|
||||
pub width: u16,
|
||||
@@ -446,7 +633,8 @@ pub struct Mode {
|
||||
}
|
||||
|
||||
/// Logical output in the compositor's coordinate space.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct LogicalOutput {
|
||||
/// Logical X position.
|
||||
pub x: i32,
|
||||
@@ -465,6 +653,7 @@ pub struct LogicalOutput {
|
||||
/// Output transform, which goes counter-clockwise.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum Transform {
|
||||
/// Untransformed.
|
||||
Normal,
|
||||
@@ -492,15 +681,31 @@ pub enum Transform {
|
||||
|
||||
/// Toplevel window.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Window {
|
||||
/// Unique id of this window.
|
||||
///
|
||||
/// This id remains constant while this window is open.
|
||||
///
|
||||
/// Do not assume that window ids will always increase without wrapping, or start at 1. That is
|
||||
/// an implementation detail subject to change. For example, ids may change to be randomly
|
||||
/// generated for each new window.
|
||||
pub id: u64,
|
||||
/// Title, if set.
|
||||
pub title: Option<String>,
|
||||
/// Application ID, if set.
|
||||
pub app_id: Option<String>,
|
||||
/// Id of the workspace this window is on, if any.
|
||||
pub workspace_id: Option<u64>,
|
||||
/// Whether this window is currently focused.
|
||||
///
|
||||
/// There can be either one focused window or zero (e.g. when a layer-shell surface has focus).
|
||||
pub is_focused: bool,
|
||||
}
|
||||
|
||||
/// Output configuration change result.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum OutputConfigChanged {
|
||||
/// The target output was connected and the change was applied.
|
||||
Applied,
|
||||
@@ -510,10 +715,24 @@ pub enum OutputConfigChanged {
|
||||
|
||||
/// A workspace.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct Workspace {
|
||||
/// Unique id of this workspace.
|
||||
///
|
||||
/// This id remains constant regardless of the workspace moving around and across monitors.
|
||||
///
|
||||
/// Do not assume that workspace ids will always increase without wrapping, or start at 1. That
|
||||
/// is an implementation detail subject to change. For example, ids may change to be randomly
|
||||
/// generated for each new workspace.
|
||||
pub id: u64,
|
||||
/// Index of the workspace on its monitor.
|
||||
///
|
||||
/// This is the same index you can use for requests like `niri msg action focus-workspace`.
|
||||
///
|
||||
/// This index *will change* as you move and re-order workspace. It is merely the workspace's
|
||||
/// current position on its monitor. Workspaces on different monitors can have the same index.
|
||||
///
|
||||
/// If you need a unique workspace id that doesn't change, see [`Self::id`].
|
||||
pub idx: u8,
|
||||
/// Optional name of the workspace.
|
||||
pub name: Option<String>,
|
||||
@@ -522,7 +741,96 @@ pub struct Workspace {
|
||||
/// Can be `None` if no outputs are currently connected.
|
||||
pub output: Option<String>,
|
||||
/// Whether the workspace is currently active on its output.
|
||||
///
|
||||
/// Every output has one active workspace, the one that is currently visible on that output.
|
||||
pub is_active: bool,
|
||||
/// Whether the workspace is currently focused.
|
||||
///
|
||||
/// There's only one focused workspace across all outputs.
|
||||
pub is_focused: bool,
|
||||
/// Id of the active window on this workspace, if any.
|
||||
pub active_window_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// Configured keyboard layouts.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct KeyboardLayouts {
|
||||
/// XKB names of the configured layouts.
|
||||
pub names: Vec<String>,
|
||||
/// Index of the currently active layout in `names`.
|
||||
pub current_idx: u8,
|
||||
}
|
||||
|
||||
/// A compositor event.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub enum Event {
|
||||
/// The workspace configuration has changed.
|
||||
WorkspacesChanged {
|
||||
/// The new workspace configuration.
|
||||
///
|
||||
/// This configuration completely replaces the previous configuration. I.e. if any
|
||||
/// workspaces are missing from here, then they were deleted.
|
||||
workspaces: Vec<Workspace>,
|
||||
},
|
||||
/// A workspace was activated on an output.
|
||||
///
|
||||
/// This doesn't always mean the workspace became focused, just that it's now the active
|
||||
/// workspace on its output. All other workspaces on the same output become inactive.
|
||||
WorkspaceActivated {
|
||||
/// Id of the newly active workspace.
|
||||
id: u64,
|
||||
/// Whether this workspace also became focused.
|
||||
///
|
||||
/// If `true`, this is now the single focused workspace. All other workspaces are no longer
|
||||
/// focused, but they may remain active on their respective outputs.
|
||||
focused: bool,
|
||||
},
|
||||
/// An active window changed on a workspace.
|
||||
WorkspaceActiveWindowChanged {
|
||||
/// Id of the workspace on which the active window changed.
|
||||
workspace_id: u64,
|
||||
/// Id of the new active window, if any.
|
||||
active_window_id: Option<u64>,
|
||||
},
|
||||
/// The window configuration has changed.
|
||||
WindowsChanged {
|
||||
/// The new window configuration.
|
||||
///
|
||||
/// This configuration completely replaces the previous configuration. I.e. if any windows
|
||||
/// are missing from here, then they were closed.
|
||||
windows: Vec<Window>,
|
||||
},
|
||||
/// A new toplevel window was opened, or an existing toplevel window changed.
|
||||
WindowOpenedOrChanged {
|
||||
/// The new or updated window.
|
||||
///
|
||||
/// If the window is focused, all other windows are no longer focused.
|
||||
window: Window,
|
||||
},
|
||||
/// A toplevel window was closed.
|
||||
WindowClosed {
|
||||
/// Id of the removed window.
|
||||
id: u64,
|
||||
},
|
||||
/// Window focus changed.
|
||||
///
|
||||
/// All other windows are no longer focused.
|
||||
WindowFocusChanged {
|
||||
/// Id of the newly focused window, or `None` if no window is now focused.
|
||||
id: Option<u64>,
|
||||
},
|
||||
/// The configured keyboard layouts have changed.
|
||||
KeyboardLayoutsChanged {
|
||||
/// The new keyboard layout configuration.
|
||||
keyboard_layouts: KeyboardLayouts,
|
||||
},
|
||||
/// The keyboard layout switched.
|
||||
KeyboardLayoutSwitched {
|
||||
/// Index of the newly active layout.
|
||||
idx: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for WorkspaceReferenceArg {
|
||||
@@ -533,7 +841,7 @@ impl FromStr for WorkspaceReferenceArg {
|
||||
if let Ok(idx) = u8::try_from(index) {
|
||||
Self::Index(idx)
|
||||
} else {
|
||||
return Err("workspace indexes must be between 0 and 255");
|
||||
return Err("workspace index must be between 0 and 255");
|
||||
}
|
||||
} else {
|
||||
Self::Name(s.to_string())
|
||||
|
||||
+23
-9
@@ -1,12 +1,12 @@
|
||||
//! Helper for blocking communication over the niri socket.
|
||||
|
||||
use std::env;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Reply, Request};
|
||||
use crate::{Event, Reply, Request};
|
||||
|
||||
/// Name of the environment variable containing the niri IPC socket path.
|
||||
pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET";
|
||||
@@ -47,17 +47,31 @@ impl Socket {
|
||||
/// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri
|
||||
/// * `Ok(Err(message))`: error message from niri
|
||||
/// * `Err(error)`: error communicating with niri
|
||||
pub fn send(self, request: Request) -> io::Result<Reply> {
|
||||
///
|
||||
/// This method also returns a blocking function that you can call to keep reading [`Event`]s
|
||||
/// after requesting an [`EventStream`][Request::EventStream]. This function is not useful
|
||||
/// otherwise.
|
||||
pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result<Event>)> {
|
||||
let Self { mut stream } = self;
|
||||
|
||||
let mut buf = serde_json::to_vec(&request).unwrap();
|
||||
stream.write_all(&buf)?;
|
||||
let mut buf = serde_json::to_string(&request).unwrap();
|
||||
stream.write_all(buf.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
buf.clear();
|
||||
stream.read_to_end(&mut buf)?;
|
||||
let mut reader = BufReader::new(stream);
|
||||
|
||||
let reply = serde_json::from_slice(&buf)?;
|
||||
Ok(reply)
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
|
||||
let events = move || {
|
||||
buf.clear();
|
||||
reader.read_line(&mut buf)?;
|
||||
let event = serde_json::from_str(&buf)?;
|
||||
Ok(event)
|
||||
};
|
||||
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Helpers for keeping track of the event stream state.
|
||||
//!
|
||||
//! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if
|
||||
//! you only care about part of the state.
|
||||
//! 2. Connect to the niri socket and request an event stream.
|
||||
//! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state.
|
||||
//! 4. Read the fields of the state as needed.
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Event, KeyboardLayouts, Window, Workspace};
|
||||
|
||||
/// Part of the state communicated via the event stream.
|
||||
pub trait EventStreamStatePart {
|
||||
/// Returns a sequence of events that replicates this state from default initialization.
|
||||
fn replicate(&self) -> Vec<Event>;
|
||||
|
||||
/// Applies the event to this state.
|
||||
///
|
||||
/// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this
|
||||
/// part of the state.
|
||||
fn apply(&mut self, event: Event) -> Option<Event>;
|
||||
}
|
||||
|
||||
/// The full state communicated over the event stream.
|
||||
///
|
||||
/// Different parts of the state are not guaranteed to be consistent across every single event
|
||||
/// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a
|
||||
/// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between
|
||||
/// these two events, the workspace active window id refers to a window that does not yet exist in
|
||||
/// the windows state part.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EventStreamState {
|
||||
/// State of workspaces.
|
||||
pub workspaces: WorkspacesState,
|
||||
|
||||
/// State of workspaces.
|
||||
pub windows: WindowsState,
|
||||
|
||||
/// State of the keyboard layouts.
|
||||
pub keyboard_layouts: KeyboardLayoutsState,
|
||||
}
|
||||
|
||||
/// The workspaces state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WorkspacesState {
|
||||
/// Map from a workspace id to the workspace.
|
||||
pub workspaces: HashMap<u64, Workspace>,
|
||||
}
|
||||
|
||||
/// The windows state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WindowsState {
|
||||
/// Map from a window id to the window.
|
||||
pub windows: HashMap<u64, Window>,
|
||||
}
|
||||
|
||||
/// The keyboard layout state communicated over the event stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct KeyboardLayoutsState {
|
||||
/// Configured keyboard layouts.
|
||||
pub keyboard_layouts: Option<KeyboardLayouts>,
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for EventStreamState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
events.extend(self.workspaces.replicate());
|
||||
events.extend(self.windows.replicate());
|
||||
events.extend(self.keyboard_layouts.replicate());
|
||||
events
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
let event = self.workspaces.apply(event)?;
|
||||
let event = self.windows.apply(event)?;
|
||||
let event = self.keyboard_layouts.apply(event)?;
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for WorkspacesState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let workspaces = self.workspaces.values().cloned().collect();
|
||||
vec![Event::WorkspacesChanged { workspaces }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect();
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let ws = self.workspaces.get(&id);
|
||||
let ws = ws.expect("activated workspace was missing from the map");
|
||||
let output = ws.output.clone();
|
||||
|
||||
for ws in self.workspaces.values_mut() {
|
||||
let got_activated = ws.id == id;
|
||||
if ws.output == output {
|
||||
ws.is_active = got_activated;
|
||||
}
|
||||
|
||||
if focused {
|
||||
ws.is_focused = got_activated;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id,
|
||||
active_window_id,
|
||||
} => {
|
||||
let ws = self.workspaces.get_mut(&workspace_id);
|
||||
let ws = ws.expect("changed workspace was missing from the map");
|
||||
ws.active_window_id = active_window_id;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for WindowsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
let windows = self.windows.values().cloned().collect();
|
||||
vec![Event::WindowsChanged { windows }]
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::WindowsChanged { windows } => {
|
||||
self.windows = windows.into_iter().map(|win| (win.id, win)).collect();
|
||||
}
|
||||
Event::WindowOpenedOrChanged { window } => {
|
||||
let (id, is_focused) = match self.windows.entry(window.id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let entry = entry.get_mut();
|
||||
*entry = window;
|
||||
(entry.id, entry.is_focused)
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let entry = entry.insert(window);
|
||||
(entry.id, entry.is_focused)
|
||||
}
|
||||
};
|
||||
|
||||
if is_focused {
|
||||
for win in self.windows.values_mut() {
|
||||
if win.id != id {
|
||||
win.is_focused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowClosed { id } => {
|
||||
let win = self.windows.remove(&id);
|
||||
win.expect("closed window was missing from the map");
|
||||
}
|
||||
Event::WindowFocusChanged { id } => {
|
||||
for win in self.windows.values_mut() {
|
||||
win.is_focused = Some(win.id) == id;
|
||||
}
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventStreamStatePart for KeyboardLayoutsState {
|
||||
fn replicate(&self) -> Vec<Event> {
|
||||
if let Some(keyboard_layouts) = self.keyboard_layouts.clone() {
|
||||
vec![Event::KeyboardLayoutsChanged { keyboard_layouts }]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Event) -> Option<Event> {
|
||||
match event {
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
self.keyboard_layouts = Some(keyboard_layouts);
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
let kb = self.keyboard_layouts.as_mut();
|
||||
let kb = kb.expect("keyboard layouts must be set before a layout can be switched");
|
||||
kb.current_idx = idx;
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
|
||||
adw = { version = "0.7.1", package = "libadwaita", features = ["v1_4"] }
|
||||
anyhow.workspace = true
|
||||
gtk = { version = "0.8.2", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.7", path = ".." }
|
||||
niri-config = { version = "0.1.7", path = "../niri-config" }
|
||||
gtk = { version = "0.9.3", package = "gtk4", features = ["v4_12"] }
|
||||
niri = { version = "0.1.10-1", path = ".." }
|
||||
niri-config = { version = "0.1.10-1", path = "../niri-config" }
|
||||
smithay.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::CornerRadius;
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
@@ -64,8 +64,9 @@ impl TestCase for GradientAngle {
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
GradientInterpolation::default(),
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
self.angle - FRAC_PI_2,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
||||
use niri::animation::ANIMATION_SLOWDOWN;
|
||||
use niri::layout::focus_ring::FocusRing;
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, FloatOrInt};
|
||||
use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
|
||||
@@ -23,7 +23,7 @@ impl GradientArea {
|
||||
let border = FocusRing::new(niri_config::FocusRing {
|
||||
off: false,
|
||||
width: FloatOrInt(1.),
|
||||
active_color: Color::new(255, 255, 255, 128),
|
||||
active_color: Color::from_rgba8_unpremul(255, 255, 255, 128),
|
||||
inactive_color: Color::default(),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
@@ -104,8 +104,9 @@ impl TestCase for GradientArea {
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
g_area,
|
||||
[1., 0., 0., 1.],
|
||||
[0., 1., 0., 1.],
|
||||
GradientInterpolation::default(),
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
FRAC_PI_4,
|
||||
Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(),
|
||||
0.,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklab {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklab {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklab {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklabAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklabAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklab,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklabAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklchAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Longer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklchDecreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchDecreasing {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Decreasing,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchDecreasing {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklchIncreasing {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchIncreasing {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Increasing,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchIncreasing {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklchLonger {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchLonger {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Longer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchLonger {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientOklchShorter {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientOklchShorter {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Oklch,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientOklchShorter {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientSrgb {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgb {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgb {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientSrgbAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::Srgb,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientSrgbLinear {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinear {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
hue_interpolation: HueInterpolation::Shorter,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbLinear {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 1.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use niri::render_helpers::border::BorderRenderElement;
|
||||
use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::utils::{Logical, Physical, Rectangle, Size};
|
||||
|
||||
use super::TestCase;
|
||||
|
||||
pub struct GradientSrgbLinearAlpha {
|
||||
gradient_format: GradientInterpolation,
|
||||
}
|
||||
|
||||
impl GradientSrgbLinearAlpha {
|
||||
pub fn new(_size: Size<i32, Logical>) -> Self {
|
||||
Self {
|
||||
gradient_format: GradientInterpolation {
|
||||
color_space: GradientColorSpace::SrgbLinear,
|
||||
hue_interpolation: Default::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase for GradientSrgbLinearAlpha {
|
||||
fn render(
|
||||
&mut self,
|
||||
_renderer: &mut GlesRenderer,
|
||||
size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
let (a, b) = (size.w / 6, size.h / 3);
|
||||
let size = (size.w - a * 2, size.h - b * 2);
|
||||
let area = Rectangle::from_loc_and_size((a, b), size).to_f64();
|
||||
|
||||
[BorderRenderElement::new(
|
||||
area.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
self.gradient_format,
|
||||
Color::new_unpremul(1., 0., 0., 1.),
|
||||
Color::new_unpremul(0., 1., 0., 0.),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), area.size),
|
||||
0.,
|
||||
CornerRadius::default(),
|
||||
1.,
|
||||
)
|
||||
.with_location(area.loc)]
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ 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, FloatOrInt};
|
||||
use niri_config::{Color, FloatOrInt, OutputName};
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
@@ -41,6 +41,12 @@ impl Layout {
|
||||
refresh: 60000,
|
||||
});
|
||||
output.change_current_state(mode, None, None, None);
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector: String::new(),
|
||||
make: None,
|
||||
model: None,
|
||||
serial: None,
|
||||
});
|
||||
|
||||
let options = Options {
|
||||
focus_ring: niri_config::FocusRing {
|
||||
@@ -50,8 +56,8 @@ impl Layout {
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: FloatOrInt(4.),
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
inactive_color: Color::new(50, 50, 50, 255),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255),
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
},
|
||||
@@ -147,7 +153,7 @@ impl Layout {
|
||||
|
||||
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false);
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false, None);
|
||||
window.communicate();
|
||||
|
||||
self.layout.add_window(window.clone(), width, false);
|
||||
@@ -161,7 +167,7 @@ impl Layout {
|
||||
width: Option<ColumnWidth>,
|
||||
) {
|
||||
let ws = self.layout.active_workspace().unwrap();
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false);
|
||||
window.request_size(ws.new_window_size(width, window.rules()), false, None);
|
||||
window.communicate();
|
||||
|
||||
self.layout
|
||||
@@ -192,11 +198,7 @@ impl TestCase for Layout {
|
||||
}
|
||||
|
||||
fn are_animations_ongoing(&self) -> bool {
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.are_animations_ongoing()
|
||||
|| !self.steps.is_empty()
|
||||
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
|
||||
}
|
||||
|
||||
fn advance_animations(&mut self, mut current_time: Duration) {
|
||||
@@ -222,12 +224,11 @@ impl TestCase for Layout {
|
||||
renderer: &mut GlesRenderer,
|
||||
_size: Size<i32, Physical>,
|
||||
) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
|
||||
self.layout.update_render_elements(&self.output);
|
||||
self.layout.update_render_elements(Some(&self.output));
|
||||
self.layout
|
||||
.monitor_for_output(&self.output)
|
||||
.unwrap()
|
||||
.render_elements(renderer, RenderTarget::Output)
|
||||
.into_iter()
|
||||
.map(|elem| Box::new(elem) as _)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,17 @@ use smithay::utils::{Physical, Size};
|
||||
|
||||
pub mod gradient_angle;
|
||||
pub mod gradient_area;
|
||||
pub mod gradient_oklab;
|
||||
pub mod gradient_oklab_alpha;
|
||||
pub mod gradient_oklch_alpha;
|
||||
pub mod gradient_oklch_decreasing;
|
||||
pub mod gradient_oklch_increasing;
|
||||
pub mod gradient_oklch_longer;
|
||||
pub mod gradient_oklch_shorter;
|
||||
pub mod gradient_srgb;
|
||||
pub mod gradient_srgb_alpha;
|
||||
pub mod gradient_srgblinear;
|
||||
pub mod gradient_srgblinear_alpha;
|
||||
pub mod layout;
|
||||
pub mod tile;
|
||||
pub mod window;
|
||||
|
||||
@@ -20,7 +20,7 @@ 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.to_f64(), false);
|
||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
@@ -28,7 +28,7 @@ impl Tile {
|
||||
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.to_f64(), false);
|
||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
@@ -37,7 +37,7 @@ impl Tile {
|
||||
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.to_f64(), false);
|
||||
rv.tile.request_tile_size(size.to_f64(), false, None);
|
||||
rv.window.communicate();
|
||||
rv
|
||||
}
|
||||
@@ -72,7 +72,7 @@ impl Tile {
|
||||
border: niri_config::Border {
|
||||
off: false,
|
||||
width: FloatOrInt(32.),
|
||||
active_color: Color::new(255, 163, 72, 255),
|
||||
active_color: Color::from_rgba8_unpremul(255, 163, 72, 255),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -85,7 +85,7 @@ impl Tile {
|
||||
impl TestCase for Tile {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.tile
|
||||
.request_tile_size(Size::from((width, height)).to_f64(), false);
|
||||
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ pub struct Window {
|
||||
impl Window {
|
||||
pub fn freeform(size: Size<i32, Logical>) -> Self {
|
||||
let mut window = TestWindow::freeform(0);
|
||||
window.request_size(size, false);
|
||||
window.request_size(size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
|
||||
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.request_size(size, false);
|
||||
window.request_size(size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
@@ -29,7 +29,7 @@ impl Window {
|
||||
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
|
||||
let mut window = TestWindow::fixed_size(0);
|
||||
window.set_csd_shadow_width(64);
|
||||
window.request_size(size, false);
|
||||
window.request_size(size, false, None);
|
||||
window.communicate();
|
||||
Self { window }
|
||||
}
|
||||
@@ -37,7 +37,8 @@ impl Window {
|
||||
|
||||
impl TestCase for Window {
|
||||
fn resize(&mut self, width: i32, height: i32) {
|
||||
self.window.request_size(Size::from((width, height)), false);
|
||||
self.window
|
||||
.request_size(Size::from((width, height)), false, None);
|
||||
self.window.communicate();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ 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,
|
||||
};
|
||||
@@ -18,7 +16,20 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::cases::gradient_angle::GradientAngle;
|
||||
use crate::cases::gradient_area::GradientArea;
|
||||
use crate::cases::gradient_oklab::GradientOklab;
|
||||
use crate::cases::gradient_oklab_alpha::GradientOklabAlpha;
|
||||
use crate::cases::gradient_oklch_alpha::GradientOklchAlpha;
|
||||
use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing;
|
||||
use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing;
|
||||
use crate::cases::gradient_oklch_longer::GradientOklchLonger;
|
||||
use crate::cases::gradient_oklch_shorter::GradientOklchShorter;
|
||||
use crate::cases::gradient_srgb::GradientSrgb;
|
||||
use crate::cases::gradient_srgb_alpha::GradientSrgbAlpha;
|
||||
use crate::cases::gradient_srgblinear::GradientSrgbLinear;
|
||||
use crate::cases::gradient_srgblinear_alpha::GradientSrgbLinearAlpha;
|
||||
use crate::cases::layout::Layout;
|
||||
use crate::cases::tile::Tile;
|
||||
use crate::cases::window::Window;
|
||||
use crate::cases::TestCase;
|
||||
|
||||
mod cases;
|
||||
@@ -112,6 +123,17 @@ fn build_ui(app: &adw::Application) {
|
||||
|
||||
s.add(GradientAngle::new, "Gradient - Angle");
|
||||
s.add(GradientArea::new, "Gradient - Area");
|
||||
s.add(GradientSrgb::new, "Gradient - Srgb");
|
||||
s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear");
|
||||
s.add(GradientOklab::new, "Gradient - Oklab");
|
||||
s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter");
|
||||
s.add(GradientOklchLonger::new, "Gradient - Oklch Longer");
|
||||
s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing");
|
||||
s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing");
|
||||
s.add(GradientSrgbAlpha::new, "Gradient - Srgb Alpha");
|
||||
s.add(GradientSrgbLinearAlpha::new, "Gradient - SrgbLinear Alpha");
|
||||
s.add(GradientOklabAlpha::new, "Gradient - Oklab Alpha");
|
||||
s.add(GradientOklchAlpha::new, "Gradient - Oklch Alpha");
|
||||
|
||||
let content_headerbar = adw::HeaderBar::new();
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ mod imp {
|
||||
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::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer, Unbind};
|
||||
use smithay::utils::{Physical, Rectangle, Scale, Transform};
|
||||
|
||||
use super::*;
|
||||
@@ -147,7 +147,7 @@ mod imp {
|
||||
.context("error creating frame")?;
|
||||
|
||||
frame
|
||||
.clear([0.3, 0.3, 0.3, 1.], &[rect])
|
||||
.clear(Color32F::from([0.3, 0.3, 0.3, 1.]), &[rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements.iter().rev() {
|
||||
@@ -186,13 +186,8 @@ mod imp {
|
||||
|
||||
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")?;
|
||||
let mut renderer = GlesRenderer::new(egl_context).context("error creating GlesRenderer")?;
|
||||
|
||||
resources::init(&mut renderer);
|
||||
shaders::init(&mut renderer);
|
||||
|
||||
@@ -3,11 +3,13 @@ use std::cmp::{max, min};
|
||||
use std::rc::Rc;
|
||||
|
||||
use niri::layout::{
|
||||
InteractiveResizeData, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot,
|
||||
ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement,
|
||||
LayoutElementRenderSnapshot,
|
||||
};
|
||||
use niri::render_helpers::renderer::NiriRenderer;
|
||||
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use niri::render_helpers::{RenderTarget, SplitElements};
|
||||
use niri::utils::transaction::Transaction;
|
||||
use niri::window::ResolvedWindowRules;
|
||||
use smithay::backend::renderer::element::{Id, Kind};
|
||||
use smithay::output::{self, Output};
|
||||
@@ -85,7 +87,7 @@ impl TestWindow {
|
||||
|
||||
let mut new_size = inner.size;
|
||||
|
||||
if let Some(size) = inner.requested_size.take() {
|
||||
if let Some(size) = inner.requested_size {
|
||||
assert!(size.w >= 0);
|
||||
assert!(size.h >= 0);
|
||||
|
||||
@@ -176,7 +178,12 @@ impl LayoutElement for TestWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
|
||||
fn request_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
_animate: bool,
|
||||
_transaction: Option<Transaction>,
|
||||
) {
|
||||
self.inner.borrow_mut().requested_size = Some(size);
|
||||
self.inner.borrow_mut().pending_fullscreen = false;
|
||||
}
|
||||
@@ -215,6 +222,10 @@ impl LayoutElement for TestWindow {
|
||||
|
||||
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
|
||||
|
||||
fn configure_intent(&self) -> ConfigureIntent {
|
||||
ConfigureIntent::CanSend
|
||||
}
|
||||
|
||||
fn send_pending_configure(&mut self) {}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
@@ -225,6 +236,10 @@ impl LayoutElement for TestWindow {
|
||||
self.inner.borrow().pending_fullscreen
|
||||
}
|
||||
|
||||
fn requested_size(&self) -> Option<Size<i32, Logical>> {
|
||||
self.inner.borrow().requested_size
|
||||
}
|
||||
|
||||
fn refresh(&self) {}
|
||||
|
||||
fn rules(&self) -> &ResolvedWindowRules {
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
%bcond_without check
|
||||
|
||||
%global cargo_install_lib 0
|
||||
|
||||
# We want panic backtraces to work without installing the debuginfo package,
|
||||
# so we leave the debuginfo in the main binary.
|
||||
%global debug_package %{nil}
|
||||
%global __strip /bin/true
|
||||
|
||||
# To reduce the file size, do some convincing of rust-srpm-macros
|
||||
# to leave alone the chosen debug settings from Cargo.toml.
|
||||
%global rustflags_debuginfo please-remove-me
|
||||
%global build_rustflags %{shrink:
|
||||
-Copt-level=%rustflags_opt_level
|
||||
-Ccodegen-units=%rustflags_codegen_units
|
||||
-Cstrip=none
|
||||
%{expr:0%{?_include_frame_pointers} && ("%{_arch}" != "ppc64le" && "%{_arch}" != "s390x" && "%{_arch}" != "i386") ? "-Cforce-frame-pointers=yes" : ""}
|
||||
-Clink-arg=-Wl,-z,relro
|
||||
-Clink-arg=-Wl,-z,now
|
||||
%[0%{?_package_note_status} ? "-Clink-arg=%_package_note_flags" : ""]
|
||||
--cap-lints=warn
|
||||
}
|
||||
|
||||
# Convince rust-srpm-macros to use Cargo.lock with the Smithay commit.
|
||||
%global __cargo_common_opts %{?_smp_mflags} -Z avoid-dev-deps --locked
|
||||
|
||||
%global version {{{ git_dir_version }}}
|
||||
|
||||
Name: niri
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: Scrollable-tiling Wayland compositor
|
||||
|
||||
SourceLicense: GPL-3.0-or-later
|
||||
|
||||
# (MIT OR Apache-2.0) AND BSD-3-Clause
|
||||
# 0BSD OR MIT OR Apache-2.0
|
||||
# Apache-2.0
|
||||
# Apache-2.0 OR BSL-1.0
|
||||
# Apache-2.0 OR MIT
|
||||
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
|
||||
# BSD-2-Clause
|
||||
# BSD-2-Clause OR Apache-2.0 OR MIT
|
||||
# BSD-3-Clause
|
||||
# BSD-3-Clause OR MIT OR Apache-2.0
|
||||
# GPL-3.0-or-later
|
||||
# ISC
|
||||
# MIT
|
||||
# MIT OR Apache-2.0
|
||||
# MIT OR Apache-2.0 OR Zlib
|
||||
# MIT OR Zlib OR Apache-2.0
|
||||
# MPL-2.0
|
||||
# Unlicense OR MIT
|
||||
# Zlib OR Apache-2.0 OR MIT
|
||||
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unlicense OR MIT) AND (Zlib OR Apache-2.0 OR MIT)
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://github.com/YaLTeR/niri
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
|
||||
BuildRequires: cargo-rpm-macros >= 26
|
||||
BuildRequires: pkgconfig(udev)
|
||||
BuildRequires: pkgconfig(gbm)
|
||||
BuildRequires: pkgconfig(xkbcommon)
|
||||
BuildRequires: wayland-devel
|
||||
BuildRequires: pkgconfig(libinput)
|
||||
BuildRequires: pkgconfig(dbus-1)
|
||||
BuildRequires: pkgconfig(systemd)
|
||||
BuildRequires: pkgconfig(libseat)
|
||||
BuildRequires: pkgconfig(libdisplay-info)
|
||||
BuildRequires: pipewire-devel
|
||||
BuildRequires: pango-devel
|
||||
BuildRequires: cairo-gobject-devel
|
||||
# Needed for pipewire-rs
|
||||
BuildRequires: clang
|
||||
|
||||
Requires: mesa-dri-drivers
|
||||
Requires: mesa-libEGL
|
||||
|
||||
# Portal implementations used by niri
|
||||
Recommends: xdg-desktop-portal-gtk
|
||||
Recommends: xdg-desktop-portal-gnome
|
||||
Recommends: gnome-keyring
|
||||
|
||||
# Suggested utilities, bound in the default config
|
||||
Recommends: alacritty
|
||||
Recommends: fuzzel
|
||||
Recommends: swaylock
|
||||
# Suggested utilities
|
||||
Recommends: swaybg
|
||||
Recommends: mako
|
||||
Recommends: swayidle
|
||||
|
||||
%description
|
||||
A scrollable-tiling Wayland compositor.
|
||||
|
||||
Windows are arranged in columns on an infinite strip going to the right.
|
||||
Opening a new window never causes existing windows to resize.
|
||||
|
||||
%prep
|
||||
{{{ git_dir_setup_macro }}}
|
||||
|
||||
# Make the version log message look nicer: since we're building not from niri's git repository,
|
||||
# the git version macro will show its fallback string.
|
||||
sed -i 's/"unknown commit"/"%{version}"/' src/utils/mod.rs
|
||||
|
||||
%cargo_prep -N
|
||||
|
||||
# We're doing an online build.
|
||||
sed -i 's/^offline = true$//' .cargo/config.toml
|
||||
|
||||
# Final step in leaving alone our debug settings.
|
||||
sed -i 's/.*please-remove-me$//' .cargo/config.toml
|
||||
|
||||
%build
|
||||
%cargo_build
|
||||
|
||||
%install
|
||||
%cargo_install
|
||||
|
||||
install -Dm755 -t %{buildroot}%{_bindir} ./resources/niri-session
|
||||
install -Dm644 -t %{buildroot}%{_datadir}/wayland-sessions ./resources/niri.desktop
|
||||
install -Dm644 -t %{buildroot}%{_datadir}/xdg-desktop-portal ./resources/niri-portals.conf
|
||||
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri.service
|
||||
install -Dm644 -t %{buildroot}%{_userunitdir} ./resources/niri-shutdown.target
|
||||
|
||||
%if %{with check}
|
||||
%check
|
||||
%cargo_test -- --workspace --exclude niri-visual-tests
|
||||
%endif
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md
|
||||
%doc resources/default-config.kdl
|
||||
%doc wiki
|
||||
%{_bindir}/niri
|
||||
%{_bindir}/niri-session
|
||||
%{_datadir}/wayland-sessions/niri.desktop
|
||||
%dir %{_datadir}/xdg-desktop-portal
|
||||
%{_datadir}/xdg-desktop-portal/niri-portals.conf
|
||||
%{_userunitdir}/niri.service
|
||||
%{_userunitdir}/niri-shutdown.target
|
||||
|
||||
%changelog
|
||||
{{{ git_dir_changelog }}}
|
||||
|
||||
@@ -40,11 +40,22 @@ input {
|
||||
// scroll-method "no-scroll"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
// off
|
||||
// natural-scroll
|
||||
// accel-speed 0.2
|
||||
// accel-profile "flat"
|
||||
// scroll-method "on-button-down"
|
||||
// scroll-button 273
|
||||
// middle-emulation
|
||||
}
|
||||
|
||||
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||
// warp-mouse-to-focus
|
||||
|
||||
// Focus windows and outputs automatically when moving the mouse into them.
|
||||
// focus-follows-mouse
|
||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||
// focus-follows-mouse max-scroll-amount="0%"
|
||||
}
|
||||
|
||||
// You can configure outputs by their name, which you can find
|
||||
@@ -112,6 +123,9 @@ layout {
|
||||
// fixed 1920
|
||||
}
|
||||
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
|
||||
// You can change the default width of the new windows.
|
||||
default-column-width { proportion 0.5; }
|
||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||
@@ -152,6 +166,7 @@ layout {
|
||||
// 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.
|
||||
// Changing the color space is also supported, check the wiki for more info.
|
||||
//
|
||||
// active-gradient from="#80c8ff" to="#bbddff" angle=45
|
||||
|
||||
@@ -197,7 +212,9 @@ layout {
|
||||
|
||||
// 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.
|
||||
// Additionally, clients will be informed that they are tiled, removing some rounded corners.
|
||||
// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners.
|
||||
// This option will also fix border/focus ring drawing behind some semitransparent windows.
|
||||
// After enabling or disabling this, you need to restart the apps for this to take effect.
|
||||
// prefer-no-csd
|
||||
|
||||
// You can change the path where screenshots are saved.
|
||||
@@ -245,6 +262,13 @@ window-rule {
|
||||
// block-out-from "screencast"
|
||||
}
|
||||
|
||||
// Example: enable rounded corners for all windows.
|
||||
// (This example rule is commented out with a "/-" in front.)
|
||||
/-window-rule {
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
}
|
||||
|
||||
binds {
|
||||
// Keys consist of modifiers separated by + signs, followed by an XKB key name
|
||||
// in the end. To find an XKB name for a particular key, you may use a program
|
||||
@@ -417,15 +441,18 @@ binds {
|
||||
// Switches focus between the current and the previous workspace.
|
||||
// Mod+Tab { focus-workspace-previous; }
|
||||
|
||||
// Consume one window from the right into the focused column.
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
// Expel one window from the focused column to the right.
|
||||
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+BracketLeft { consume-or-expel-window-left; }
|
||||
Mod+BracketRight { consume-or-expel-window-right; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { reset-window-height; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
Mod+C { center-column; }
|
||||
@@ -459,6 +486,7 @@ binds {
|
||||
|
||||
// The quit action will show a confirmation dialog to avoid accidental exits.
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete { quit; }
|
||||
|
||||
// Powers off the monitors. To turn them back on, do any input like
|
||||
// moving the mouse or pressing any other key.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
type = process
|
||||
command = niri --session
|
||||
restart = false
|
||||
working-dir = $HOME
|
||||
depends-on = dbus
|
||||
after = niri-shutdown
|
||||
chain-to = niri-shutdown
|
||||
options: always-chain
|
||||
@@ -0,0 +1,3 @@
|
||||
type = scripted
|
||||
command = dinitctl -u setenv WAYLAND_DISPLAY= XDG_SESSION_TYPE= XDG_CURRENT_DESKTOP= NIRI_SOCKET=
|
||||
restart = false
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="mutter_x11_interop">
|
||||
<description summary="X11 interoperability helper">
|
||||
This protocol is intended to be used by the portal backend to map Wayland
|
||||
dialogs as modal dialogs on top of X11 windows.
|
||||
</description>
|
||||
|
||||
<interface name="mutter_x11_interop" version="1">
|
||||
<description summary="X11 interoperability helper"/>
|
||||
|
||||
<request name="destroy" type="destructor"/>
|
||||
|
||||
<request name="set_x11_parent">
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
<arg name="xwindow" type="uint"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -1,3 +1,4 @@
|
||||
[preferred]
|
||||
default=gnome;gtk;
|
||||
org.freedesktop.impl.portal.Access=gtk;
|
||||
org.freedesktop.impl.portal.Secret=gnome-keyring;
|
||||
|
||||
+47
-27
@@ -11,31 +11,51 @@ if [ -n "$SHELL" ] &&
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
# Try to detect the service manager that is being used
|
||||
if hash systemctl &> /dev/null; then
|
||||
# Make sure there's no already running session.
|
||||
if systemctl --user -q is-active niri.service; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of graphical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
elif hash dinitctl &> /dev/null; then
|
||||
# Check that the user dinit daemon is running
|
||||
if ! pgrep -u $(id -u) dinit &> /dev/null; then
|
||||
echo "dinit user daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure there's no already running session.
|
||||
if dinitctl --user is-started niri &> /dev/null; then
|
||||
echo 'A niri session is already running.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start niri
|
||||
dinitctl --user start niri
|
||||
else
|
||||
echo "No systemd or dinit detected, please use niri --session instead."
|
||||
fi
|
||||
|
||||
# Reset failed state of all user units.
|
||||
systemctl --user reset-failed
|
||||
|
||||
# Import the login manager environment.
|
||||
systemctl --user import-environment
|
||||
|
||||
# DBus activation environment is independent from systemd. While most of
|
||||
# dbus-activated services are already using `SystemdService` directive, some
|
||||
# still don't and thus we should set the dbus environment with a separate
|
||||
# command.
|
||||
if hash dbus-update-activation-environment 2>/dev/null; then
|
||||
dbus-update-activation-environment --all
|
||||
fi
|
||||
|
||||
# Start niri and wait for it to terminate.
|
||||
systemctl --user --wait start niri.service
|
||||
|
||||
# Force stop of graphical-session.target.
|
||||
systemctl --user start --job-mode=replace-irreversibly niri-shutdown.target
|
||||
|
||||
# Unset environment that we've set.
|
||||
systemctl --user unset-environment WAYLAND_DISPLAY XDG_SESSION_TYPE XDG_CURRENT_DESKTOP NIRI_SOCKET
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=refresh content=0;url=niri_ipc/index.html />
|
||||
</head>
|
||||
</html>
|
||||
@@ -11,7 +11,7 @@ pub use spring::{Spring, SpringParams};
|
||||
|
||||
pub static ANIMATION_SLOWDOWN: AtomicF64 = AtomicF64::new(1.);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Animation {
|
||||
from: f64,
|
||||
to: f64,
|
||||
@@ -101,9 +101,9 @@ impl Animation {
|
||||
}
|
||||
|
||||
/// Restarts the animation using the previous config.
|
||||
pub fn restarted(self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||
pub fn restarted(&self, from: f64, to: f64, initial_velocity: f64) -> Self {
|
||||
if self.is_off {
|
||||
return self;
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
// Scale the velocity by slowdown to keep the touchpad gestures feeling right.
|
||||
@@ -292,7 +292,7 @@ impl Animation {
|
||||
return self.to;
|
||||
}
|
||||
|
||||
let passed = self.current_time - self.start_time;
|
||||
let passed = self.current_time.saturating_sub(self.start_time);
|
||||
|
||||
match self.kind {
|
||||
Kind::Easing { curve } => {
|
||||
|
||||
+32
-1
@@ -9,6 +9,7 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
||||
use crate::input::CompositorMod;
|
||||
use crate::niri::Niri;
|
||||
use crate::utils::id::IdCounter;
|
||||
|
||||
pub mod tty;
|
||||
pub use tty::Tty;
|
||||
@@ -31,7 +32,22 @@ pub enum RenderResult {
|
||||
Skipped,
|
||||
}
|
||||
|
||||
pub type IpcOutputMap = HashMap<String, niri_ipc::Output>;
|
||||
pub type IpcOutputMap = HashMap<OutputId, niri_ipc::Output>;
|
||||
|
||||
static OUTPUT_ID_COUNTER: IdCounter = IdCounter::new();
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct OutputId(u64);
|
||||
|
||||
impl OutputId {
|
||||
fn next() -> OutputId {
|
||||
OutputId(OUTPUT_ID_COUNTER.next())
|
||||
}
|
||||
|
||||
pub fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub fn init(&mut self, niri: &mut Niri) {
|
||||
@@ -137,6 +153,13 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
|
||||
Backend::Winit(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.on_output_config_changed(niri),
|
||||
@@ -151,6 +174,14 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
|
||||
if let Self::Tty(v) = self {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tty(&mut self) -> &mut Tty {
|
||||
if let Self::Tty(v) = self {
|
||||
v
|
||||
|
||||
+442
-229
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -5,7 +5,7 @@ use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::Config;
|
||||
use niri_config::{Config, OutputName};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -17,7 +17,7 @@ use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_pre
|
||||
use smithay::reexports::winit::dpi::LogicalSize;
|
||||
use smithay::reexports::winit::window::Window;
|
||||
|
||||
use super::{IpcOutputMap, RenderResult};
|
||||
use super::{IpcOutputMap, OutputId, RenderResult};
|
||||
use crate::niri::{Niri, RedrawState, State};
|
||||
use crate::render_helpers::debug::draw_damage;
|
||||
use crate::render_helpers::{resources, shaders, RenderTarget};
|
||||
@@ -59,13 +59,21 @@ impl Winit {
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.user_data().insert_if_missing(|| OutputName {
|
||||
connector: "winit".to_string(),
|
||||
make: Some("Smithay".to_string()),
|
||||
model: Some("Winit".to_string()),
|
||||
serial: None,
|
||||
});
|
||||
|
||||
let physical_properties = output.physical_properties();
|
||||
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
|
||||
"winit".to_owned(),
|
||||
OutputId::next(),
|
||||
niri_ipc::Output {
|
||||
name: output.name(),
|
||||
make: physical_properties.make,
|
||||
model: physical_properties.model,
|
||||
serial: None,
|
||||
physical_size: None,
|
||||
modes: vec![niri_ipc::Mode {
|
||||
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,
|
||||
@@ -98,7 +106,7 @@ impl Winit {
|
||||
|
||||
{
|
||||
let mut ipc_outputs = winit.ipc_outputs.lock().unwrap();
|
||||
let output = ipc_outputs.get_mut("winit").unwrap();
|
||||
let output = ipc_outputs.values_mut().next().unwrap();
|
||||
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;
|
||||
|
||||
+8
-2
@@ -62,10 +62,14 @@ pub enum Msg {
|
||||
Outputs,
|
||||
/// List workspaces.
|
||||
Workspaces,
|
||||
/// Print information about the focused window.
|
||||
FocusedWindow,
|
||||
/// List open windows.
|
||||
Windows,
|
||||
/// Get the configured keyboard layouts.
|
||||
KeyboardLayouts,
|
||||
/// Print information about the focused output.
|
||||
FocusedOutput,
|
||||
/// Print information about the focused window.
|
||||
FocusedWindow,
|
||||
/// Perform an action.
|
||||
Action {
|
||||
#[command(subcommand)]
|
||||
@@ -86,6 +90,8 @@ pub enum Msg {
|
||||
#[command(subcommand)]
|
||||
action: OutputAction,
|
||||
},
|
||||
/// Start continuously receiving events from the compositor.
|
||||
EventStream,
|
||||
/// Print the version of the running niri instance.
|
||||
Version,
|
||||
/// Request an error from the running niri instance.
|
||||
|
||||
@@ -8,6 +8,7 @@ use zbus::{dbus_interface, fdo, SignalContext};
|
||||
|
||||
use super::Start;
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::utils::is_laptop_panel;
|
||||
|
||||
pub struct DisplayConfig {
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
@@ -57,24 +58,20 @@ impl DisplayConfig {
|
||||
.ipc_outputs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.values()
|
||||
// Take only enabled outputs.
|
||||
.filter(|(_, output)| output.current_mode.is_some() && output.logical.is_some())
|
||||
.map(|(c, output)| {
|
||||
.filter(|output| output.current_mode.is_some() && output.logical.is_some())
|
||||
.map(|output| {
|
||||
// Loosely matches the check in Mutter.
|
||||
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
|
||||
|
||||
// FIXME: use proper serial when we have libdisplay-info.
|
||||
// A serial is required for correct session restore by xdp-gnome.
|
||||
let serial = c.clone();
|
||||
let c = &output.name;
|
||||
let is_laptop_panel = is_laptop_panel(c);
|
||||
let display_name = make_display_name(output, is_laptop_panel);
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
if is_laptop_panel {
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
|
||||
);
|
||||
}
|
||||
properties.insert(
|
||||
String::from("display-name"),
|
||||
OwnedValue::from(zvariant::Str::from(display_name)),
|
||||
);
|
||||
properties.insert(
|
||||
String::from("is-builtin"),
|
||||
OwnedValue::from(is_laptop_panel),
|
||||
@@ -110,8 +107,16 @@ impl DisplayConfig {
|
||||
.properties
|
||||
.insert(String::from("is-current"), OwnedValue::from(true));
|
||||
|
||||
let connector = c.clone();
|
||||
let model = output.model.clone();
|
||||
let make = output.make.clone();
|
||||
|
||||
// Serial is used for session restore, so fall back to the connector name if it's
|
||||
// not available.
|
||||
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
|
||||
|
||||
let monitor = Monitor {
|
||||
names: (c.clone(), String::new(), String::new(), serial),
|
||||
names: (connector, make, model, serial),
|
||||
modes,
|
||||
properties,
|
||||
};
|
||||
@@ -143,15 +148,8 @@ impl DisplayConfig {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort the built-in monitor first, then by connector name.
|
||||
monitors.sort_unstable_by(|a, b| {
|
||||
let a_is_builtin = a.0.properties.contains_key("display-name");
|
||||
let b_is_builtin = b.0.properties.contains_key("display-name");
|
||||
a_is_builtin
|
||||
.cmp(&b_is_builtin)
|
||||
.reverse()
|
||||
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
|
||||
});
|
||||
// Sort by connector.
|
||||
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
|
||||
|
||||
let (monitors, logical_monitors) = monitors.into_iter().unzip();
|
||||
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
|
||||
@@ -182,3 +180,48 @@ impl Start for DisplayConfig {
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from Mutter.
|
||||
fn make_display_name(output: &niri_ipc::Output, is_laptop_panel: bool) -> String {
|
||||
if is_laptop_panel {
|
||||
return String::from("Built-in display");
|
||||
}
|
||||
|
||||
let make = &output.make;
|
||||
let model = &output.model;
|
||||
if let Some(diagonal) = output.physical_size.map(|(width_mm, height_mm)| {
|
||||
let diagonal = f64::hypot(f64::from(width_mm), f64::from(height_mm)) / 25.4;
|
||||
format_diagonal(diagonal)
|
||||
}) {
|
||||
format!("{make} {diagonal}")
|
||||
} else if model != "Unknown" {
|
||||
format!("{make} {model}")
|
||||
} else {
|
||||
make.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_diagonal(diagonal_inches: f64) -> String {
|
||||
let known = [12.1, 13.3, 15.6];
|
||||
if let Some(d) = known.iter().find(|d| (*d - diagonal_inches).abs() < 0.1) {
|
||||
format!("{d:.1}″")
|
||||
} else {
|
||||
format!("{}″", diagonal_inches.round() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use k9::snapshot;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_diagonal() {
|
||||
snapshot!(format_diagonal(12.11), "12.1″");
|
||||
snapshot!(format_diagonal(13.28), "13.3″");
|
||||
snapshot!(format_diagonal(15.6), "15.6″");
|
||||
snapshot!(format_diagonal(23.2), "23″");
|
||||
snapshot!(format_diagonal(24.8), "25″");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,11 @@ impl Session {
|
||||
) -> fdo::Result<OwnedObjectPath> {
|
||||
debug!(connector, ?properties, "record_monitor");
|
||||
|
||||
let Some(output) = self.ipc_outputs.lock().unwrap().get(connector).cloned() else {
|
||||
let output = {
|
||||
let ipc_outputs = self.ipc_outputs.lock().unwrap();
|
||||
ipc_outputs.values().find(|o| o.name == connector).cloned()
|
||||
};
|
||||
let Some(output) = output else {
|
||||
return Err(fdo::Error::Failed("no such monitor".to_owned()));
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ impl FrameClock {
|
||||
self.last_presentation_time = None;
|
||||
}
|
||||
|
||||
pub fn vrr(&self) -> bool {
|
||||
self.vrr
|
||||
}
|
||||
|
||||
pub fn presented(&mut self, presentation_time: Duration) {
|
||||
if presentation_time.is_zero() {
|
||||
// Not interested in these.
|
||||
|
||||
+128
-44
@@ -1,15 +1,16 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state};
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||
use smithay::reexports::calloop::Interest;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{Client, Resource};
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, with_states,
|
||||
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
|
||||
add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, remove_pre_commit_hook,
|
||||
with_states, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState,
|
||||
SurfaceAttributes,
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
@@ -19,6 +20,7 @@ use smithay::{delegate_compositor, delegate_shm};
|
||||
use super::xdg_shell::add_mapped_toplevel_pre_commit_hook;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::utils::send_scale_transform;
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped};
|
||||
|
||||
impl CompositorHandler for State {
|
||||
@@ -46,43 +48,12 @@ impl CompositorHandler for State {
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: &WlSurface) {
|
||||
add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
.cached_state
|
||||
.pending::<SurfaceAttributes>()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
if let Some(client) = surface.client() {
|
||||
let res =
|
||||
state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
let _span = tracy_client::span!("CompositorHandler::commit");
|
||||
trace!(surface = ?surface.id(), "commit");
|
||||
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.backend.early_import(surface);
|
||||
@@ -156,6 +127,8 @@ impl CompositorHandler for State {
|
||||
})
|
||||
.map(|(mapped, _)| mapped.window.clone());
|
||||
|
||||
// The mapped pre-commit hook deals with dma-bufs on its own.
|
||||
self.remove_default_dmabuf_pre_commit_hook(toplevel.wl_surface());
|
||||
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
|
||||
let mapped = Mapped::new(window, rules, hook);
|
||||
let window = mapped.window.clone();
|
||||
@@ -221,11 +194,13 @@ impl CompositorHandler for State {
|
||||
});
|
||||
|
||||
// Must start the close animation before window.on_commit().
|
||||
let transaction = Transaction::new();
|
||||
if !is_mapped {
|
||||
let blocker = transaction.blocker();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri
|
||||
.layout
|
||||
.start_close_animation_for_window(renderer, &window);
|
||||
.start_close_animation_for_window(renderer, &window, blocker);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,10 +216,17 @@ impl CompositorHandler for State {
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.niri
|
||||
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
|
||||
id: u64::from(id.get()),
|
||||
id: id.get(),
|
||||
});
|
||||
|
||||
self.niri.layout.remove_window(&window);
|
||||
self.niri.layout.remove_window(&window, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface);
|
||||
|
||||
// If this is the only instance, then this transaction will complete
|
||||
// immediately, so no need to set the timer.
|
||||
if !transaction.is_last() {
|
||||
transaction.register_deadline_timer(&self.niri.event_loop);
|
||||
}
|
||||
|
||||
if was_active {
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
@@ -302,22 +284,68 @@ impl CompositorHandler for State {
|
||||
if let Some(output) = self.output_for_popup(&popup) {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a layer-shell surface.
|
||||
self.layer_shell_handle_commit(surface);
|
||||
if self.layer_shell_handle_commit(surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a cursor surface.
|
||||
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
|
||||
{
|
||||
if matches!(
|
||||
&self.niri.cursor_manager.cursor_image(),
|
||||
CursorImageStatus::Surface(s) if s == &root_surface
|
||||
) {
|
||||
// In case the cursor surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the cursor image hotspot
|
||||
if surface == &root_surface {
|
||||
with_states(surface, |states| {
|
||||
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
|
||||
|
||||
if let Some(mut cursor_image_attributes) =
|
||||
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
|
||||
{
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
if let Some(buffer_delta) = buffer_delta {
|
||||
cursor_image_attributes.hotspot -= buffer_delta;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a DnD icon surface.
|
||||
if self.niri.dnd_icon.as_ref() == Some(surface) {
|
||||
if matches!(&self.niri.dnd_icon, Some(icon) if icon.surface == root_surface) {
|
||||
let dnd_icon = self.niri.dnd_icon.as_mut().unwrap();
|
||||
|
||||
// In case the dnd surface has been committed handle the role specific
|
||||
// buffer offset by applying the offset on the dnd icon offset
|
||||
if surface == &dnd_icon.surface {
|
||||
with_states(&dnd_icon.surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
dnd_icon.offset += buffer_delta;
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: granular redraws for cursors.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
|
||||
// This might be a lock surface.
|
||||
@@ -326,7 +354,7 @@ impl CompositorHandler for State {
|
||||
if let Some(lock_surface) = &state.lock_surface {
|
||||
if lock_surface.wl_surface() == &root_surface {
|
||||
self.niri.queue_redraw(&output.clone());
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +383,8 @@ impl CompositorHandler for State {
|
||||
self.niri
|
||||
.root_surface
|
||||
.retain(|k, v| k != surface && v != surface);
|
||||
|
||||
self.niri.dmabuf_pre_commit_hook.remove(surface);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,3 +400,57 @@ impl ShmHandler for State {
|
||||
|
||||
delegate_compositor!(State);
|
||||
delegate_shm!(State);
|
||||
|
||||
impl State {
|
||||
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
surface_data
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.pending()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
if let Some(client) = surface.client() {
|
||||
let res =
|
||||
state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
trace!("added default dmabuf blocker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let s = surface.clone();
|
||||
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
|
||||
error!("tried to add dmabuf pre-commit hook when there was already one");
|
||||
remove_pre_commit_hook(surface, prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
|
||||
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
|
||||
remove_pre_commit_hook(surface, hook);
|
||||
} else {
|
||||
error!("tried to remove dmabuf pre-commit hook but there was none");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
-31
@@ -1,11 +1,12 @@
|
||||
use smithay::backend::renderer::utils::with_renderer_surface_state;
|
||||
use smithay::delegate_layer_shell;
|
||||
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
@@ -36,12 +37,19 @@ impl WlrLayerShellHandler for State {
|
||||
return;
|
||||
};
|
||||
|
||||
let wl_surface = surface.wl_surface().clone();
|
||||
let is_new = self.niri.unmapped_layer_surfaces.insert(wl_surface);
|
||||
assert!(is_new);
|
||||
|
||||
let mut map = layer_map_for_output(&output);
|
||||
map.map_layer(&LayerSurface::new(surface, namespace))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
|
||||
let wl_surface = surface.wl_surface();
|
||||
self.niri.unmapped_layer_surfaces.remove(wl_surface);
|
||||
|
||||
let output = if let Some((output, mut map, layer)) =
|
||||
self.niri.layout.outputs().find_map(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
@@ -68,52 +76,101 @@ impl WlrLayerShellHandler for State {
|
||||
delegate_layer_shell!(State);
|
||||
|
||||
impl State {
|
||||
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) {
|
||||
let Some(output) = self
|
||||
pub fn layer_shell_handle_commit(&mut self, surface: &WlSurface) -> bool {
|
||||
let mut root_surface = surface.clone();
|
||||
while let Some(parent) = get_parent(&root_surface) {
|
||||
root_surface = parent;
|
||||
}
|
||||
|
||||
let output = self
|
||||
.niri
|
||||
.layout
|
||||
.outputs()
|
||||
.find(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
map.layer_for_surface(&root_surface, WindowSurfaceType::TOPLEVEL)
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
.cloned();
|
||||
let Some(output) = output else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
if surface == &root_surface {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
|
||||
let mut map = layer_map_for_output(&output);
|
||||
let mut map = layer_map_for_output(&output);
|
||||
|
||||
// Arrange the layers before sending the initial configure to respect any size the
|
||||
// client may have sent.
|
||||
map.arrange();
|
||||
|
||||
// arrange the layers before sending the initial configure
|
||||
// to respect any size the client may have sent
|
||||
map.arrange();
|
||||
// send the initial configure if relevant
|
||||
if !initial_configure_sent {
|
||||
let layer = map
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
.unwrap();
|
||||
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_scale_transform(surface, data, scale, transform);
|
||||
});
|
||||
if initial_configure_sent {
|
||||
let is_mapped =
|
||||
with_renderer_surface_state(surface, |state| state.buffer().is_some())
|
||||
.unwrap_or_else(|| {
|
||||
error!("no renderer surface state even though we use commit handler");
|
||||
false
|
||||
});
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
if is_mapped {
|
||||
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);
|
||||
|
||||
// Give focus to newly mapped on-demand surfaces. Some launchers like
|
||||
// lxqt-runner rely on this behavior. While this behavior doesn't make much
|
||||
// sense for other clients like panels, the consensus seems to be that it's not
|
||||
// a big deal since panels generally only open once at the start of the
|
||||
// session.
|
||||
//
|
||||
// Note that:
|
||||
// 1) Exclusive layer surfaces already get focus automatically in
|
||||
// update_keyboard_focus().
|
||||
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand
|
||||
// surfaces in update_keyboard_focus(), so we don't need to check for that
|
||||
// here.
|
||||
//
|
||||
// https://github.com/YaLTeR/niri/issues/641
|
||||
let on_demand = layer.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::OnDemand;
|
||||
if was_unmapped && on_demand {
|
||||
// I guess it'd make sense to check that no higher-layer on-demand surface
|
||||
// has focus, but Smithay's Layer doesn't implement Ord so this would be a
|
||||
// little annoying.
|
||||
self.niri.layer_shell_on_demand_focus = Some(layer.clone());
|
||||
}
|
||||
} else {
|
||||
self.niri.unmapped_layer_surfaces.insert(surface.clone());
|
||||
}
|
||||
} else {
|
||||
let scale = output.current_scale();
|
||||
let transform = output.current_transform();
|
||||
with_states(surface, |data| {
|
||||
send_scale_transform(surface, data, scale, transform);
|
||||
});
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
}
|
||||
drop(map);
|
||||
|
||||
// This will call queue_redraw() inside.
|
||||
self.niri.output_resized(&output);
|
||||
} else {
|
||||
// This is an unsync layer-shell subsurface.
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
drop(map);
|
||||
|
||||
self.niri.output_resized(&output);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
+146
-29
@@ -7,22 +7,26 @@ use std::io::Write;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::drm::DrmNode;
|
||||
use smithay::backend::input::TabletToolDescriptor;
|
||||
use smithay::desktop::{PopupKind, PopupManager};
|
||||
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
|
||||
use smithay::input::pointer::{
|
||||
CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle,
|
||||
};
|
||||
use smithay::input::{keyboard, Seat, SeatHandler, SeatState};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{fcntl_setfl, OFlags};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Logical, Rectangle, Size};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::wayland::compositor::{get_parent, with_states};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::drm_lease::{
|
||||
DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
||||
@@ -32,7 +36,7 @@ use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler};
|
||||
use smithay::wayland::security_context::{
|
||||
SecurityContext, SecurityContextHandler, SecurityContextListenerSource,
|
||||
};
|
||||
@@ -63,14 +67,21 @@ use smithay::{
|
||||
};
|
||||
|
||||
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
|
||||
use crate::niri::{ClientState, State};
|
||||
use crate::niri::{ClientState, DndIcon, 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, send_scale_transform};
|
||||
use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy};
|
||||
use crate::protocols::mutter_x11_interop::MutterX11InteropHandler;
|
||||
use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState};
|
||||
use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState};
|
||||
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
|
||||
use crate::{
|
||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
|
||||
delegate_output_management, delegate_screencopy,
|
||||
};
|
||||
|
||||
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
impl SeatHandler for State {
|
||||
type KeyboardFocus = WlSurface;
|
||||
@@ -129,11 +140,66 @@ impl TabletSeatHandler for State {
|
||||
delegate_tablet_manager!(State);
|
||||
|
||||
impl PointerConstraintsHandler for State {
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
self.niri.maybe_activate_pointer_constraint(
|
||||
pointer.current_location(),
|
||||
&self.niri.pointer_focus,
|
||||
);
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, _pointer: &PointerHandle<Self>) {
|
||||
// Pointer constraints track pointer focus internally, so make sure it's up to date before
|
||||
// activating a new one.
|
||||
self.refresh_pointer_contents();
|
||||
|
||||
self.niri.maybe_activate_pointer_constraint();
|
||||
}
|
||||
|
||||
fn cursor_position_hint(
|
||||
&mut self,
|
||||
surface: &WlSurface,
|
||||
pointer: &PointerHandle<Self>,
|
||||
location: Point<f64, Logical>,
|
||||
) {
|
||||
let is_constraint_active = with_pointer_constraint(surface, pointer, |constraint| {
|
||||
constraint.map_or(false, |c| c.is_active())
|
||||
});
|
||||
|
||||
if !is_constraint_active {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: this is surface under pointer, not pointer focus. So if you start, say, a
|
||||
// middle-drag in Blender, then touchpad-swipe the window away, the surface under pointer
|
||||
// will change, even though the real pointer focus remains on the Blender surface due to
|
||||
// the click grab.
|
||||
//
|
||||
// Ideally we would just use the constraint surface, but we need its origin. So this is
|
||||
// more of a hack because pointer contents has the surface origin available.
|
||||
//
|
||||
// FIXME: use the constraint surface somehow, don't use pointer contents.
|
||||
let Some((ref surface_under_pointer, origin)) = self.niri.pointer_contents.surface else {
|
||||
return;
|
||||
};
|
||||
|
||||
if surface_under_pointer != surface {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
let target = self
|
||||
.niri
|
||||
.output_for_root(&root)
|
||||
.and_then(|output| self.niri.global_space.output_geometry(output))
|
||||
.map_or(origin + location, |mut output_geometry| {
|
||||
// i32 sizes are exclusive, but f64 sizes are inclusive.
|
||||
output_geometry.size -= (1, 1).into();
|
||||
(origin + location).constrain(output_geometry.to_f64())
|
||||
});
|
||||
pointer.set_location(target);
|
||||
|
||||
// Redraw to update the cursor position if it's visible.
|
||||
if !self.niri.pointer_hidden {
|
||||
// FIXME: redraw only outputs overlapping the cursor.
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate_pointer_constraints!(State);
|
||||
@@ -219,7 +285,23 @@ impl ClientDndGrabHandler for State {
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
) {
|
||||
self.niri.dnd_icon = icon;
|
||||
let offset = if let CursorImageStatus::Surface(ref surface) =
|
||||
self.niri.cursor_manager.cursor_image()
|
||||
{
|
||||
with_states(surface, |states| {
|
||||
let hotspot = states
|
||||
.data_map
|
||||
.get::<CursorImageSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hotspot;
|
||||
Point::from((-hotspot.x, -hotspot.y))
|
||||
})
|
||||
} else {
|
||||
(0, 0).into()
|
||||
};
|
||||
self.niri.dnd_icon = icon.map(|surface| DndIcon { surface, offset });
|
||||
// FIXME: more granular
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
@@ -368,6 +450,7 @@ impl ForeignToplevelHandler for State {
|
||||
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.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
@@ -381,12 +464,12 @@ impl ForeignToplevelHandler for State {
|
||||
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)
|
||||
{
|
||||
let has_fullscreen_cap = with_toplevel_role(mapped.toplevel(), |role| {
|
||||
role.current
|
||||
.capabilities
|
||||
.contains(xdg_toplevel::WmCapabilities::Fullscreen)
|
||||
});
|
||||
if !has_fullscreen_cap {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -396,7 +479,7 @@ impl ForeignToplevelHandler for State {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(&window, &requested_output);
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,14 +497,30 @@ impl ForeignToplevelHandler for State {
|
||||
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:?}");
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
|
||||
// If with_damage then push it onto the queue for redraw of the output,
|
||||
// otherwise render it immediately.
|
||||
if screencopy.with_damage() {
|
||||
let Some(queue) = self.niri.screencopy_state.get_queue_mut(manager) else {
|
||||
trace!("screencopy manager destroyed already");
|
||||
return;
|
||||
};
|
||||
queue.push(screencopy);
|
||||
} else {
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
if let Err(err) = self
|
||||
.niri
|
||||
.render_for_screencopy_without_damage(renderer, manager, screencopy)
|
||||
{
|
||||
warn!("error rendering for screencopy: {err:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState {
|
||||
&mut self.niri.screencopy_state
|
||||
}
|
||||
}
|
||||
delegate_screencopy!(State);
|
||||
|
||||
@@ -528,15 +627,18 @@ impl XdgActivationHandler for State {
|
||||
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
_token: XdgActivationToken,
|
||||
token: XdgActivationToken,
|
||||
token_data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
if token_data.timestamp.elapsed().as_secs() < 10 {
|
||||
if token_data.timestamp.elapsed() < XDG_ACTIVATION_TOKEN_TIMEOUT {
|
||||
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(&surface) {
|
||||
let window = mapped.window.clone();
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
self.niri.activation_state.remove_token(&token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,3 +647,18 @@ delegate_xdg_activation!(State);
|
||||
|
||||
impl FractionalScaleHandler for State {}
|
||||
delegate_fractional_scale!(State);
|
||||
|
||||
impl OutputManagementHandler for State {
|
||||
fn output_management_state(&mut self) -> &mut OutputManagementManagerState {
|
||||
&mut self.niri.output_management_state
|
||||
}
|
||||
|
||||
fn apply_output_config(&mut self, config: niri_config::Outputs) {
|
||||
self.niri.config.borrow_mut().outputs = config;
|
||||
self.reload_output_config();
|
||||
}
|
||||
}
|
||||
delegate_output_management!(State);
|
||||
|
||||
impl MutterX11InteropHandler for State {}
|
||||
delegate_mutter_x11_interop!(State);
|
||||
|
||||
+265
-71
@@ -1,5 +1,6 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use calloop::Interest;
|
||||
use smithay::desktop::{
|
||||
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, utils, LayerSurface,
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
|
||||
@@ -17,26 +18,34 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{self, Resource, WEnum};
|
||||
use smithay::utils::{Logical, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::{
|
||||
add_pre_commit_hook, with_states, BufferAssignment, HookId, SurfaceAttributes,
|
||||
add_blocker, add_pre_commit_hook, with_states, BufferAssignment, CompositorHandler as _,
|
||||
HookId, SurfaceAttributes,
|
||||
};
|
||||
use smithay::wayland::dmabuf::get_dmabuf;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::selection::data_device::DnDGrab;
|
||||
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
|
||||
use smithay::wayland::shell::wlr_layer::{self, Layer};
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler,
|
||||
XdgShellState, XdgToplevelSurfaceData,
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
XdgToplevelSurfaceData,
|
||||
};
|
||||
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
|
||||
use smithay::{
|
||||
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
|
||||
};
|
||||
use tracing::field::Empty;
|
||||
|
||||
use crate::input::move_grab::MoveGrab;
|
||||
use crate::input::resize_grab::ResizeGrab;
|
||||
use crate::input::DOUBLE_CLICK_TIME;
|
||||
use crate::input::touch_move_grab::TouchMoveGrab;
|
||||
use crate::input::touch_resize_grab::TouchResizeGrab;
|
||||
use crate::input::{PointerOrTouchStartData, DOUBLE_CLICK_TIME};
|
||||
use crate::layout::workspace::ColumnWidth;
|
||||
use crate::niri::{PopupGrabState, State};
|
||||
use crate::utils::{get_monotonic_time, send_scale_transform, ResizeEdge};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
|
||||
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
|
||||
|
||||
impl XdgShellHandler for State {
|
||||
@@ -60,8 +69,94 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
|
||||
// FIXME
|
||||
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
|
||||
let wl_surface = surface.wl_surface();
|
||||
|
||||
let mut grab_start_data = None;
|
||||
|
||||
// See if this comes from a pointer grab.
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
pointer.with_grab(|grab_serial, grab| {
|
||||
if grab_serial == serial {
|
||||
let start_data = grab.start_data();
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
Some(PointerOrTouchStartData::Pointer(start_data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// See if this comes from a touch grab.
|
||||
if let Some(touch) = self.niri.seat.get_touch() {
|
||||
touch.with_grab(|grab_serial, grab| {
|
||||
if grab_serial == serial {
|
||||
let start_data = grab.start_data();
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
// Deny move requests from DnD grabs to work around
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/issues/7113
|
||||
let is_dnd_grab = grab.as_any().is::<DnDGrab<Self>>();
|
||||
|
||||
if !is_dnd_grab {
|
||||
grab_start_data =
|
||||
Some(PointerOrTouchStartData::Touch(start_data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let Some(start_data) = grab_start_data else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((mapped, output)) = self.niri.layout.find_window_and_output(wl_surface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let window = mapped.window.clone();
|
||||
let output = output.clone();
|
||||
|
||||
let output_pos = self
|
||||
.niri
|
||||
.global_space
|
||||
.output_geometry(&output)
|
||||
.unwrap()
|
||||
.loc
|
||||
.to_f64();
|
||||
|
||||
let pos_within_output = start_data.location() - output_pos;
|
||||
|
||||
if !self
|
||||
.niri
|
||||
.layout
|
||||
.interactive_move_begin(window.clone(), &output, pos_within_output)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = MoveGrab::new(start_data, window);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
let touch = self.niri.seat.get_touch().unwrap();
|
||||
let grab = TouchMoveGrab::new(start_data, window);
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
fn resize_request(
|
||||
@@ -71,24 +166,39 @@ impl XdgShellHandler for State {
|
||||
serial: Serial,
|
||||
edges: xdg_toplevel::ResizeEdge,
|
||||
) {
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
if !pointer.has_grab(serial) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(start_data) = pointer.grab_start_data() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((focus, _)) = &start_data.focus else {
|
||||
return;
|
||||
};
|
||||
|
||||
let wl_surface = surface.wl_surface();
|
||||
if !focus.id().same_client_as(&wl_surface.id()) {
|
||||
return;
|
||||
|
||||
let mut grab_start_data = None;
|
||||
|
||||
// See if this comes from a pointer grab.
|
||||
let pointer = self.niri.seat.get_pointer().unwrap();
|
||||
if pointer.has_grab(serial) {
|
||||
if let Some(start_data) = pointer.grab_start_data() {
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
grab_start_data = Some(PointerOrTouchStartData::Pointer(start_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See if this comes from a touch grab.
|
||||
if let Some(touch) = self.niri.seat.get_touch() {
|
||||
if touch.has_grab(serial) {
|
||||
if let Some(start_data) = touch.grab_start_data() {
|
||||
if let Some((focus, _)) = &start_data.focus {
|
||||
if focus.id().same_client_as(&wl_surface.id()) {
|
||||
grab_start_data = Some(PointerOrTouchStartData::Touch(start_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(start_data) = grab_start_data else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((mapped, _)) = self.niri.layout.find_window_and_output(wl_surface) else {
|
||||
return;
|
||||
};
|
||||
@@ -110,12 +220,12 @@ impl XdgShellHandler for State {
|
||||
if intersection.intersects(ResizeEdge::LEFT_RIGHT) {
|
||||
// FIXME: don't activate once we can pass specific windows to actions.
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.layout.toggle_full_width();
|
||||
}
|
||||
if intersection.intersects(ResizeEdge::TOP_BOTTOM) {
|
||||
// FIXME: don't activate once we can pass specific windows to actions.
|
||||
self.niri.layout.activate_window(&window);
|
||||
self.niri.layout.reset_window_height();
|
||||
self.niri.layer_shell_on_demand_focus = None;
|
||||
self.niri.layout.reset_window_height(Some(&window));
|
||||
}
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -123,14 +233,25 @@ impl XdgShellHandler for State {
|
||||
}
|
||||
}
|
||||
|
||||
let grab = ResizeGrab::new(start_data, window.clone());
|
||||
|
||||
if !self.niri.layout.interactive_resize_begin(window, edges) {
|
||||
if !self
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_begin(window.clone(), edges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri.pointer_grab_ongoing = true;
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = ResizeGrab::new(start_data, window);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
let touch = self.niri.seat.get_touch().unwrap();
|
||||
let grab = TouchResizeGrab::new(start_data, window);
|
||||
touch.set_grab(self, grab, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reposition_request(
|
||||
@@ -184,10 +305,13 @@ impl XdgShellHandler for State {
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: popup grabs for on-demand bottom and background layers.
|
||||
} else {
|
||||
if layers.layers_on(Layer::Overlay).any(|l| {
|
||||
l.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::Exclusive
|
||||
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|
||||
}) {
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
return;
|
||||
@@ -198,6 +322,7 @@ impl XdgShellHandler for State {
|
||||
&& layers.layers_on(Layer::Top).any(|l| {
|
||||
l.cached_state().keyboard_interactivity
|
||||
== wlr_layer::KeyboardInteractivity::Exclusive
|
||||
|| Some(l) == self.niri.layer_shell_on_demand_focus.as_ref()
|
||||
})
|
||||
{
|
||||
let _ = PopupManager::dismiss_popup(&root, &popup);
|
||||
@@ -252,7 +377,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if initial_configure_sent(&surface) {
|
||||
if surface.is_initial_configure_sent() {
|
||||
surface.send_configure();
|
||||
}
|
||||
}
|
||||
@@ -279,7 +404,7 @@ impl XdgShellHandler for State {
|
||||
if &requested_output != current_output {
|
||||
self.niri
|
||||
.layout
|
||||
.move_window_to_output(&window, &requested_output);
|
||||
.move_to_output(Some(&window), &requested_output, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +448,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
.map(|(mon, _)| mon.output().clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = mon
|
||||
@@ -407,7 +532,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
*output = mon
|
||||
.filter(|(_, parent)| !parent)
|
||||
.map(|(mon, _)| mon.output.clone());
|
||||
.map(|(mon, _)| mon.output().clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let ws = workspace_name
|
||||
@@ -469,22 +594,32 @@ impl XdgShellHandler for State {
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
self.niri
|
||||
.stop_casts_for_target(crate::pw_utils::CastTarget::Window {
|
||||
id: u64::from(mapped.id().get()),
|
||||
id: mapped.id().get(),
|
||||
});
|
||||
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri.layout.store_unmap_snapshot(renderer, &window);
|
||||
});
|
||||
|
||||
let transaction = Transaction::new();
|
||||
let blocker = transaction.blocker();
|
||||
self.backend.with_primary_renderer(|renderer| {
|
||||
self.niri
|
||||
.layout
|
||||
.start_close_animation_for_window(renderer, &window);
|
||||
.start_close_animation_for_window(renderer, &window, blocker);
|
||||
});
|
||||
|
||||
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, transaction.clone());
|
||||
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
|
||||
|
||||
// If this is the only instance, then this transaction will complete immediately, so no
|
||||
// need to set the timer.
|
||||
if !transaction.is_last() {
|
||||
transaction.register_deadline_timer(&self.niri.event_loop);
|
||||
}
|
||||
|
||||
if was_active {
|
||||
self.maybe_warp_cursor_to_focus();
|
||||
@@ -533,7 +668,7 @@ impl XdgDecorationHandler for State {
|
||||
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if initial_configure_sent(&toplevel) {
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
@@ -546,7 +681,7 @@ impl XdgDecorationHandler for State {
|
||||
|
||||
// A configure is required in response to this event. However, if an initial configure
|
||||
// wasn't sent, then we will send this as part of the initial configure later.
|
||||
if initial_configure_sent(&toplevel) {
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
@@ -601,18 +736,6 @@ impl XdgForeignHandler for State {
|
||||
}
|
||||
delegate_xdg_foreign!(State);
|
||||
|
||||
fn initial_configure_sent(toplevel: &ToplevelSurface) -> bool {
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
})
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn send_initial_configure(&mut self, toplevel: &ToplevelSurface) {
|
||||
let _span = tracy_client::span!("State::send_initial_configure");
|
||||
@@ -647,7 +770,12 @@ impl State {
|
||||
rules
|
||||
.open_on_output
|
||||
.as_deref()
|
||||
.and_then(|name| self.niri.output_by_name.get(name))
|
||||
.and_then(|name| {
|
||||
self.niri
|
||||
.global_space
|
||||
.outputs()
|
||||
.find(|output| output_matches_name(output, name))
|
||||
})
|
||||
.and_then(|o| self.niri.layout.monitor_for_output(o))
|
||||
});
|
||||
|
||||
@@ -682,7 +810,7 @@ impl State {
|
||||
// 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());
|
||||
.map(|(mon, _)| mon.output().clone());
|
||||
let mon = mon.map(|(mon, _)| mon);
|
||||
|
||||
let mut width = None;
|
||||
@@ -735,7 +863,7 @@ impl State {
|
||||
width,
|
||||
is_full_width,
|
||||
output,
|
||||
workspace_name: ws.and_then(|w| w.name.clone()),
|
||||
workspace_name: ws.and_then(|w| w.name().cloned()),
|
||||
};
|
||||
|
||||
toplevel.send_configure();
|
||||
@@ -764,16 +892,7 @@ impl State {
|
||||
if let Some(popup) = self.niri.popups.find_popup(surface) {
|
||||
match popup {
|
||||
PopupKind::Xdg(ref popup) => {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgPopupSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
if !initial_configure_sent {
|
||||
if !popup.is_initial_configure_sent() {
|
||||
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
|
||||
{
|
||||
let scale = output.current_scale();
|
||||
@@ -993,15 +1112,26 @@ fn unconstrain_with_padding(
|
||||
pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId {
|
||||
add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| {
|
||||
let _span = tracy_client::span!("mapped toplevel pre-commit");
|
||||
let span =
|
||||
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
|
||||
|
||||
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
|
||||
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
|
||||
return;
|
||||
};
|
||||
|
||||
let (got_unmapped, commit_serial) = with_states(surface, |states| {
|
||||
let attrs = states.cached_state.pending::<SurfaceAttributes>();
|
||||
let got_unmapped = matches!(attrs.buffer, Some(BufferAssignment::Removed));
|
||||
let (got_unmapped, dmabuf, commit_serial) = with_states(surface, |states| {
|
||||
let (got_unmapped, dmabuf) = {
|
||||
let mut guard = states.cached_state.get::<SurfaceAttributes>();
|
||||
match guard.pending().buffer.as_ref() {
|
||||
Some(BufferAssignment::NewBuffer(buffer)) => {
|
||||
let dmabuf = get_dmabuf(buffer).cloned().ok();
|
||||
(false, dmabuf)
|
||||
}
|
||||
Some(BufferAssignment::Removed) => (true, None),
|
||||
None => (false, None),
|
||||
}
|
||||
};
|
||||
|
||||
let role = states
|
||||
.data_map
|
||||
@@ -1010,16 +1140,80 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
|
||||
.lock()
|
||||
.unwrap();
|
||||
|
||||
(got_unmapped, role.configure_serial)
|
||||
(got_unmapped, dmabuf, role.configure_serial)
|
||||
});
|
||||
|
||||
let animate = if let Some(serial) = commit_serial {
|
||||
mapped.should_animate_commit(serial)
|
||||
let mut transaction_for_dmabuf = None;
|
||||
let mut animate = false;
|
||||
if let Some(serial) = commit_serial {
|
||||
if !span.is_disabled() {
|
||||
span.record("serial", format!("{serial:?}"));
|
||||
}
|
||||
|
||||
trace!("taking pending transaction");
|
||||
if let Some(transaction) = mapped.take_pending_transaction(serial) {
|
||||
// Transaction can be already completed if it ran past the deadline.
|
||||
let disable = state.niri.config.borrow().debug.disable_transactions;
|
||||
if !transaction.is_completed() && !disable {
|
||||
// Register the deadline even if this is the last pending, since dmabuf
|
||||
// rendering can still run over the deadline.
|
||||
transaction.register_deadline_timer(&state.niri.event_loop);
|
||||
|
||||
let is_last = transaction.is_last();
|
||||
|
||||
// If this is the last transaction, we don't need to add a separate
|
||||
// notification, because the transaction will complete in our dmabuf blocker
|
||||
// callback, which already calls blocker_cleared(), or by the end of this
|
||||
// function, in which case there would be no blocker in the first place.
|
||||
if !is_last {
|
||||
// Waiting for some other surface; register a notification and add a
|
||||
// transaction blocker.
|
||||
if let Some(client) = surface.client() {
|
||||
transaction.add_notification(
|
||||
state.niri.blocker_cleared_tx.clone(),
|
||||
client.clone(),
|
||||
);
|
||||
add_blocker(surface, transaction.blocker());
|
||||
}
|
||||
}
|
||||
|
||||
// Delay dropping (and completing) the transaction until the dmabuf is ready.
|
||||
// If there's no dmabuf, this will be dropped by the end of this pre-commit
|
||||
// hook.
|
||||
transaction_for_dmabuf = Some(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
animate = mapped.should_animate_commit(serial);
|
||||
} else {
|
||||
error!("commit on a mapped surface without a configured serial");
|
||||
false
|
||||
};
|
||||
|
||||
if let Some((blocker, source)) =
|
||||
dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok())
|
||||
{
|
||||
if let Some(client) = surface.client() {
|
||||
let res = state
|
||||
.niri
|
||||
.event_loop
|
||||
.insert_source(source, move |_, _, state| {
|
||||
// This surface is now ready for the transaction.
|
||||
drop(transaction_for_dmabuf.take());
|
||||
|
||||
let display_handle = state.niri.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &display_handle);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
trace!("added dmabuf blocker");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let window = mapped.window.clone();
|
||||
if got_unmapped {
|
||||
state.backend.with_primary_renderer(|renderer| {
|
||||
|
||||
+686
-114
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
is_moving: bool,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
is_moving: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::default_named());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<State> for MoveGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.motion(data, None, event);
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
let timestamp = Duration::from_millis(u64::from(event.time));
|
||||
if self.is_moving {
|
||||
data.niri.layout.view_offset_gesture_update(
|
||||
-event_delta.x,
|
||||
timestamp,
|
||||
false,
|
||||
);
|
||||
}
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus.
|
||||
handle.relative_motion(data, None, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// MouseButton::Middle
|
||||
if event.button == 0x112 {
|
||||
if event.state == ButtonState::Pressed {
|
||||
let output = data
|
||||
.niri
|
||||
.output_under(handle.current_location())
|
||||
.map(|(output, _)| output)
|
||||
.cloned();
|
||||
// FIXME: workspace switch gesture.
|
||||
if let Some(output) = output {
|
||||
self.is_moving = true;
|
||||
data.niri.layout.view_offset_gesture_begin(&output, false);
|
||||
}
|
||||
} else if event.state == ButtonState::Released {
|
||||
self.is_moving = false;
|
||||
data.niri.layout.view_offset_gesture_end(false, None);
|
||||
}
|
||||
}
|
||||
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut PointerInnerHandle<'_, State>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ impl ResizeGrab {
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
state.niri.pointer_grab_ongoing = false;
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
|
||||
@@ -50,7 +50,6 @@ impl SpatialMovementGrab {
|
||||
state.niri.queue_redraw(&output);
|
||||
}
|
||||
|
||||
state.niri.pointer_grab_ongoing = false;
|
||||
state
|
||||
.niri
|
||||
.cursor_manager
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchMoveGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchMoveGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_move_end(&self.window);
|
||||
// FIXME: only redraw the window output.
|
||||
state.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchMoveGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
output,
|
||||
pos_within_output,
|
||||
);
|
||||
if ongoing {
|
||||
// FIXME: only redraw the previous and the new output.
|
||||
data.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The move is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::touch::{
|
||||
DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
|
||||
TouchGrab, TouchInnerHandle, UpEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point, Serial};
|
||||
|
||||
use crate::niri::State;
|
||||
|
||||
pub struct TouchResizeGrab {
|
||||
start_data: TouchGrabStartData<State>,
|
||||
window: Window,
|
||||
}
|
||||
|
||||
impl TouchResizeGrab {
|
||||
pub fn new(start_data: TouchGrabStartData<State>, window: Window) -> Self {
|
||||
Self { start_data, window }
|
||||
}
|
||||
|
||||
fn on_ungrab(&mut self, state: &mut State) {
|
||||
state.niri.layout.interactive_resize_end(&self.window);
|
||||
}
|
||||
}
|
||||
|
||||
impl TouchGrab<State> for TouchResizeGrab {
|
||||
fn down(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &DownEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.down(data, None, event, seq);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &UpEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.up(data, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
_focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.motion(data, None, event, seq);
|
||||
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.window.alive() {
|
||||
let delta = event.location - self.start_data.location;
|
||||
let ongoing = data
|
||||
.niri
|
||||
.layout
|
||||
.interactive_resize_update(&self.window, delta);
|
||||
if ongoing {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The resize is no longer ongoing.
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.frame(data, seq);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
|
||||
handle.cancel(data, seq);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &ShapeEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.shape(data, event, seq);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut State,
|
||||
handle: &mut TouchInnerHandle<'_, State>,
|
||||
event: &OrientationEvent,
|
||||
seq: Serial,
|
||||
) {
|
||||
handle.orientation(data, event, seq);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &TouchGrabStartData<State> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, data: &mut State) {
|
||||
self.on_ungrab(data);
|
||||
}
|
||||
}
|
||||
+141
-22
@@ -1,6 +1,9 @@
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::socket::Socket;
|
||||
use niri_ipc::{
|
||||
LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response, Socket, Transform,
|
||||
Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Request, Response,
|
||||
Transform, Window,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -19,12 +22,15 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
action: action.clone(),
|
||||
},
|
||||
Msg::Workspaces => Request::Workspaces,
|
||||
Msg::Windows => Request::Windows,
|
||||
Msg::KeyboardLayouts => Request::KeyboardLayouts,
|
||||
Msg::EventStream => Request::EventStream,
|
||||
Msg::RequestError => Request::ReturnError,
|
||||
};
|
||||
|
||||
let socket = Socket::connect().context("error connecting to the niri socket")?;
|
||||
|
||||
let reply = socket
|
||||
let (reply, mut read_event) = socket
|
||||
.send(request)
|
||||
.context("error communicating with niri")?;
|
||||
|
||||
@@ -35,6 +41,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
Socket::connect()
|
||||
.and_then(|socket| socket.send(Request::Version))
|
||||
.ok()
|
||||
.map(|(reply, _read_event)| reply)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -114,11 +121,14 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
|
||||
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut outputs = outputs
|
||||
.into_values()
|
||||
.map(|out| (OutputName::from_ipc_output(&out), out))
|
||||
.collect::<Vec<_>>();
|
||||
outputs.sort_unstable_by(|a, b| a.0.compare(&b.0));
|
||||
|
||||
for (connector, output) in outputs.into_iter() {
|
||||
print_output(connector, output)?;
|
||||
for (_name, output) in outputs.into_iter() {
|
||||
print_output(output)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
@@ -134,23 +144,30 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
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)");
|
||||
}
|
||||
print_window(&window);
|
||||
} else {
|
||||
println!("No window is focused.");
|
||||
}
|
||||
}
|
||||
Msg::Windows => {
|
||||
let Response::Windows(mut windows) = response else {
|
||||
bail!("unexpected response: expected Windows, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let windows =
|
||||
serde_json::to_string(&windows).context("error formatting response")?;
|
||||
println!("{windows}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
for window in windows {
|
||||
print_window(&window);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Msg::FocusedOutput => {
|
||||
let Response::FocusedOutput(output) = response else {
|
||||
bail!("unexpected response: expected FocusedOutput, got {response:?}");
|
||||
@@ -163,7 +180,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
if let Some(output) = output {
|
||||
print_output(output.name.clone(), output)?;
|
||||
print_output(output)?;
|
||||
} else {
|
||||
println!("No output is focused.");
|
||||
}
|
||||
@@ -238,16 +255,94 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
|
||||
println!("{is_active}{idx}{name}");
|
||||
}
|
||||
}
|
||||
Msg::KeyboardLayouts => {
|
||||
let Response::KeyboardLayouts(response) = response else {
|
||||
bail!("unexpected response: expected KeyboardLayouts, got {response:?}");
|
||||
};
|
||||
|
||||
if json {
|
||||
let response =
|
||||
serde_json::to_string(&response).context("error formatting response")?;
|
||||
println!("{response}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let KeyboardLayouts { names, current_idx } = response;
|
||||
let current_idx = usize::from(current_idx);
|
||||
|
||||
println!("Keyboard layouts:");
|
||||
for (idx, name) in names.iter().enumerate() {
|
||||
let is_active = if idx == current_idx { " * " } else { " " };
|
||||
println!("{is_active}{idx} {name}");
|
||||
}
|
||||
}
|
||||
Msg::EventStream => {
|
||||
let Response::Handled = response else {
|
||||
bail!("unexpected response: expected Handled, got {response:?}");
|
||||
};
|
||||
|
||||
if !json {
|
||||
println!("Started reading events.");
|
||||
}
|
||||
|
||||
loop {
|
||||
let event = read_event().context("error reading event from niri")?;
|
||||
|
||||
if json {
|
||||
let event = serde_json::to_string(&event).context("error formatting event")?;
|
||||
println!("{event}");
|
||||
continue;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::WorkspacesChanged { workspaces } => {
|
||||
println!("Workspaces changed: {workspaces:?}");
|
||||
}
|
||||
Event::WorkspaceActivated { id, focused } => {
|
||||
let word = if focused { "focused" } else { "activated" };
|
||||
println!("Workspace {word}: {id}");
|
||||
}
|
||||
Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id,
|
||||
active_window_id,
|
||||
} => {
|
||||
println!(
|
||||
"Workspace {workspace_id}: \
|
||||
active window changed to {active_window_id:?}"
|
||||
);
|
||||
}
|
||||
Event::WindowsChanged { windows } => {
|
||||
println!("Windows changed: {windows:?}");
|
||||
}
|
||||
Event::WindowOpenedOrChanged { window } => {
|
||||
println!("Window opened or changed: {window:?}");
|
||||
}
|
||||
Event::WindowClosed { id } => {
|
||||
println!("Window closed: {id}");
|
||||
}
|
||||
Event::WindowFocusChanged { id } => {
|
||||
println!("Window focus changed: {id:?}");
|
||||
}
|
||||
Event::KeyboardLayoutsChanged { keyboard_layouts } => {
|
||||
println!("Keyboard layouts changed: {keyboard_layouts:?}");
|
||||
}
|
||||
Event::KeyboardLayoutSwitched { idx } => {
|
||||
println!("Keyboard layout switched: {idx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
|
||||
fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
let Output {
|
||||
name,
|
||||
make,
|
||||
model,
|
||||
serial,
|
||||
physical_size,
|
||||
modes,
|
||||
current_mode,
|
||||
@@ -256,7 +351,8 @@ fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
|
||||
logical,
|
||||
} = output;
|
||||
|
||||
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
|
||||
let serial = serial.as_deref().unwrap_or("Unknown");
|
||||
println!(r#"Output "{make} {model} {serial}" ({name})"#);
|
||||
|
||||
if let Some(current) = current_mode {
|
||||
let mode = *modes
|
||||
@@ -336,3 +432,26 @@ fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_window(window: &Window) {
|
||||
let focused = if window.is_focused { " (focused)" } else { "" };
|
||||
println!("Window ID {}:{focused}", window.id);
|
||||
|
||||
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)");
|
||||
}
|
||||
|
||||
if let Some(workspace_id) = window.workspace_id {
|
||||
println!(" Workspace ID: {workspace_id}");
|
||||
} else {
|
||||
println!(" Workspace ID: (none)");
|
||||
}
|
||||
}
|
||||
|
||||
+391
-42
@@ -1,33 +1,58 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{env, io, process};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_channel::{Receiver, Sender, TrySendError};
|
||||
use calloop::futures::Scheduler;
|
||||
use calloop::io::Async;
|
||||
use directories::BaseDirs;
|
||||
use futures_util::io::{AsyncReadExt, BufReader};
|
||||
use futures_util::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use niri_ipc::{OutputConfigChanged, Reply, Request, Response};
|
||||
use smithay::desktop::Window;
|
||||
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
|
||||
use niri_config::OutputName;
|
||||
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
|
||||
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::rustix::fs::unlink;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
|
||||
use crate::backend::IpcOutputMap;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::version;
|
||||
use crate::utils::{version, with_toplevel_role};
|
||||
use crate::window::Mapped;
|
||||
|
||||
// If an event stream client fails to read events fast enough that we accumulate more than this
|
||||
// number in our buffer, we drop that event stream client.
|
||||
const EVENT_STREAM_BUFFER_SIZE: usize = 64;
|
||||
|
||||
pub struct IpcServer {
|
||||
pub socket_path: PathBuf,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
|
||||
struct ClientCtx {
|
||||
event_loop: LoopHandle<'static, State>,
|
||||
scheduler: Scheduler<()>,
|
||||
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
||||
ipc_focused_window: Arc<Mutex<Option<Window>>>,
|
||||
event_streams: Rc<RefCell<Vec<EventStreamSender>>>,
|
||||
event_stream_state: Rc<RefCell<EventStreamState>>,
|
||||
}
|
||||
|
||||
struct EventStreamClient {
|
||||
events: Receiver<Event>,
|
||||
disconnect: Receiver<()>,
|
||||
write: Box<dyn AsyncWrite + Unpin>,
|
||||
}
|
||||
|
||||
struct EventStreamSender {
|
||||
events: Sender<Event>,
|
||||
disconnect: Sender<()>,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
@@ -59,7 +84,34 @@ impl IpcServer {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Ok(Self { socket_path })
|
||||
Ok(Self {
|
||||
socket_path,
|
||||
event_streams: Rc::new(RefCell::new(Vec::new())),
|
||||
event_stream_state: Rc::new(RefCell::new(EventStreamState::default())),
|
||||
})
|
||||
}
|
||||
|
||||
fn send_event(&self, event: Event) {
|
||||
let mut streams = self.event_streams.borrow_mut();
|
||||
let mut to_remove = Vec::new();
|
||||
for (idx, stream) in streams.iter_mut().enumerate() {
|
||||
match stream.events.try_send(event.clone()) {
|
||||
Ok(()) => (),
|
||||
Err(TrySendError::Closed(_)) => to_remove.push(idx),
|
||||
Err(TrySendError::Full(_)) => {
|
||||
warn!(
|
||||
"disconnecting IPC event stream client \
|
||||
because it is reading events too slowly"
|
||||
);
|
||||
to_remove.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx in to_remove.into_iter().rev() {
|
||||
let stream = streams.swap_remove(idx);
|
||||
let _ = stream.disconnect.send_blocking(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +141,14 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
}
|
||||
};
|
||||
|
||||
let ipc_server = state.niri.ipc_server.as_ref().unwrap();
|
||||
|
||||
let ctx = ClientCtx {
|
||||
event_loop: state.niri.event_loop.clone(),
|
||||
scheduler: state.niri.scheduler.clone(),
|
||||
ipc_outputs: state.backend.ipc_outputs(),
|
||||
ipc_focused_window: state.niri.ipc_focused_window.clone(),
|
||||
event_streams: ipc_server.event_streams.clone(),
|
||||
event_stream_state: ipc_server.event_stream_state.clone(),
|
||||
};
|
||||
|
||||
let future = async move {
|
||||
@@ -105,7 +161,7 @@ fn on_new_ipc_client(state: &mut State, stream: UnixStream) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow::Result<()> {
|
||||
async fn handle_client(ctx: ClientCtx, stream: Async<'static, UnixStream>) -> anyhow::Result<()> {
|
||||
let (read, mut write) = stream.split();
|
||||
let mut buf = String::new();
|
||||
|
||||
@@ -119,6 +175,7 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
|
||||
.context("error parsing request")
|
||||
.map_err(|err| err.to_string());
|
||||
let requested_error = matches!(request, Ok(Request::ReturnError));
|
||||
let requested_event_stream = matches!(request, Ok(Request::EventStream));
|
||||
|
||||
let reply = match request {
|
||||
Ok(request) => process(&ctx, request).await,
|
||||
@@ -131,9 +188,50 @@ async fn handle_client(ctx: ClientCtx, stream: Async<'_, UnixStream>) -> anyhow:
|
||||
}
|
||||
}
|
||||
|
||||
let buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
let mut buf = serde_json::to_vec(&reply).context("error formatting reply")?;
|
||||
buf.push(b'\n');
|
||||
write.write_all(&buf).await.context("error writing reply")?;
|
||||
|
||||
if requested_event_stream {
|
||||
let (events_tx, events_rx) = async_channel::bounded(EVENT_STREAM_BUFFER_SIZE);
|
||||
let (disconnect_tx, disconnect_rx) = async_channel::bounded(1);
|
||||
|
||||
// Spawn a task for the client.
|
||||
let client = EventStreamClient {
|
||||
events: events_rx,
|
||||
disconnect: disconnect_rx,
|
||||
write: Box::new(write) as _,
|
||||
};
|
||||
let future = async move {
|
||||
if let Err(err) = handle_event_stream_client(client).await {
|
||||
warn!("error handling IPC event stream client: {err:?}");
|
||||
}
|
||||
};
|
||||
if let Err(err) = ctx.scheduler.schedule(future) {
|
||||
warn!("error scheduling IPC event stream future: {err:?}");
|
||||
}
|
||||
|
||||
// Send the initial state.
|
||||
{
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
for event in state.replicate() {
|
||||
events_tx
|
||||
.try_send(event)
|
||||
.expect("initial event burst had more events than buffer size");
|
||||
}
|
||||
}
|
||||
|
||||
// Add it to the list.
|
||||
{
|
||||
let mut streams = ctx.event_streams.borrow_mut();
|
||||
let sender = EventStreamSender {
|
||||
events: events_tx,
|
||||
disconnect: disconnect_tx,
|
||||
};
|
||||
streams.push(sender);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -143,26 +241,29 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
Request::Version => Response::Version(version()),
|
||||
Request::Outputs => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap().clone();
|
||||
Response::Outputs(ipc_outputs)
|
||||
let outputs = ipc_outputs.values().cloned().map(|o| (o.name.clone(), o));
|
||||
Response::Outputs(outputs.collect())
|
||||
}
|
||||
Request::Workspaces => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let workspaces = state.workspaces.workspaces.values().cloned().collect();
|
||||
Response::Workspaces(workspaces)
|
||||
}
|
||||
Request::Windows => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let windows = state.windows.windows.values().cloned().collect();
|
||||
Response::Windows(windows)
|
||||
}
|
||||
Request::KeyboardLayouts => {
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let layout = state.keyboard_layouts.keyboard_layouts.clone();
|
||||
let layout = layout.expect("keyboard layouts should be set at startup");
|
||||
Response::KeyboardLayouts(layout)
|
||||
}
|
||||
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(),
|
||||
}
|
||||
})
|
||||
});
|
||||
let state = ctx.event_stream_state.borrow();
|
||||
let windows = &state.windows.windows;
|
||||
let window = windows.values().find(|win| win.is_focused).cloned();
|
||||
Response::FocusedWindow(window)
|
||||
}
|
||||
Request::Action(action) => {
|
||||
@@ -183,8 +284,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
Request::Output { output, action } => {
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
|
||||
let found = ipc_outputs
|
||||
.keys()
|
||||
.any(|name| name.eq_ignore_ascii_case(&output));
|
||||
.values()
|
||||
.any(|o| OutputName::from_ipc_output(o).matches(&output));
|
||||
let response = if found {
|
||||
OutputConfigChanged::Applied
|
||||
} else {
|
||||
@@ -198,16 +299,6 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
|
||||
Response::OutputConfigChanged(response)
|
||||
}
|
||||
Request::Workspaces => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
let workspaces = state.niri.layout.ipc_workspaces();
|
||||
let _ = tx.send_blocking(workspaces);
|
||||
});
|
||||
let result = rx.recv().await;
|
||||
let workspaces = result.map_err(|_| String::from("error getting workspace info"))?;
|
||||
Response::Workspaces(workspaces)
|
||||
}
|
||||
Request::FocusedOutput => {
|
||||
let (tx, rx) = async_channel::bounded(1);
|
||||
ctx.event_loop.insert_idle(move |state| {
|
||||
@@ -223,7 +314,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
.ipc_outputs()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&active_output)
|
||||
.values()
|
||||
.find(|o| o.name == active_output)
|
||||
.cloned()
|
||||
});
|
||||
|
||||
@@ -233,7 +325,264 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
let output = result.map_err(|_| String::from("error getting active output info"))?;
|
||||
Response::FocusedOutput(output)
|
||||
}
|
||||
Request::EventStream => Response::Handled,
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result<()> {
|
||||
let EventStreamClient {
|
||||
events,
|
||||
disconnect,
|
||||
mut write,
|
||||
} = client;
|
||||
|
||||
while let Ok(event) = events.recv().await {
|
||||
let mut buf = serde_json::to_vec(&event).context("error formatting event")?;
|
||||
buf.push(b'\n');
|
||||
|
||||
let res = select_biased! {
|
||||
_ = disconnect.recv().fuse() => return Ok(()),
|
||||
res = write.write_all(&buf).fuse() => res,
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
// Normal client disconnection.
|
||||
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
|
||||
res @ Err(_) => res.context("error writing event")?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window {
|
||||
with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window {
|
||||
id: mapped.id().get(),
|
||||
title: role.title.clone(),
|
||||
app_id: role.app_id.clone(),
|
||||
workspace_id: workspace_id.map(|id| id.get()),
|
||||
is_focused: mapped.is_focused(),
|
||||
})
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn ipc_keyboard_layouts_changed(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let keyboard_layouts = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
let layouts = xkb.layouts();
|
||||
KeyboardLayouts {
|
||||
names: layouts
|
||||
.map(|layout| xkb.layout_name(layout).to_owned())
|
||||
.collect(),
|
||||
current_idx: xkb.active_layout().0 as u8,
|
||||
}
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.keyboard_layouts;
|
||||
|
||||
let event = Event::KeyboardLayoutsChanged { keyboard_layouts };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_keyboard_layout_index(&mut self) {
|
||||
let keyboard = self.niri.seat.get_keyboard().unwrap();
|
||||
let idx = keyboard.with_xkb_state(self, |context| {
|
||||
let xkb = context.xkb().lock().unwrap();
|
||||
xkb.active_layout().0 as u8
|
||||
});
|
||||
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.keyboard_layouts;
|
||||
|
||||
if state.keyboard_layouts.as_ref().unwrap().current_idx == idx {
|
||||
return;
|
||||
}
|
||||
|
||||
let event = Event::KeyboardLayoutSwitched { idx };
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
|
||||
pub fn ipc_refresh_layout(&mut self) {
|
||||
self.ipc_refresh_workspaces();
|
||||
self.ipc_refresh_windows();
|
||||
}
|
||||
|
||||
fn ipc_refresh_workspaces(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_workspaces");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.workspaces;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let layout = &self.niri.layout;
|
||||
let focused_ws_id = layout.active_workspace().map(|ws| ws.id().get());
|
||||
|
||||
// Check for workspace changes.
|
||||
let mut seen = HashSet::new();
|
||||
let mut need_workspaces_changed = false;
|
||||
for (mon, ws_idx, ws) in layout.workspaces() {
|
||||
let id = ws.id().get();
|
||||
seen.insert(id);
|
||||
|
||||
let Some(ipc_ws) = state.workspaces.get(&id) else {
|
||||
// A new workspace was added.
|
||||
need_workspaces_changed = true;
|
||||
break;
|
||||
};
|
||||
|
||||
// Check for any changes that we can't signal as individual events.
|
||||
let output_name = mon.map(|mon| mon.output_name());
|
||||
if ipc_ws.idx != u8::try_from(ws_idx + 1).unwrap_or(u8::MAX)
|
||||
|| ipc_ws.name.as_ref() != ws.name()
|
||||
|| ipc_ws.output.as_ref() != output_name
|
||||
{
|
||||
need_workspaces_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
let active_window_id = ws.active_window().map(|win| win.id().get());
|
||||
if ipc_ws.active_window_id != active_window_id {
|
||||
events.push(Event::WorkspaceActiveWindowChanged {
|
||||
workspace_id: id,
|
||||
active_window_id,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this workspace became focused.
|
||||
let is_focused = Some(id) == focused_ws_id;
|
||||
if is_focused && !ipc_ws.is_focused {
|
||||
events.push(Event::WorkspaceActivated { id, focused: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this workspace became active.
|
||||
let is_active = mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx);
|
||||
if is_active && !ipc_ws.is_active {
|
||||
events.push(Event::WorkspaceActivated { id, focused: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any workspaces were removed.
|
||||
if !need_workspaces_changed && state.workspaces.keys().any(|id| !seen.contains(id)) {
|
||||
need_workspaces_changed = true;
|
||||
}
|
||||
|
||||
if need_workspaces_changed {
|
||||
events.clear();
|
||||
|
||||
let workspaces = layout
|
||||
.workspaces()
|
||||
.map(|(mon, ws_idx, ws)| {
|
||||
let id = ws.id().get();
|
||||
Workspace {
|
||||
id,
|
||||
idx: u8::try_from(ws_idx + 1).unwrap_or(u8::MAX),
|
||||
name: ws.name().cloned(),
|
||||
output: mon.map(|mon| mon.output_name().clone()),
|
||||
is_active: mon.map_or(false, |mon| mon.active_workspace_idx() == ws_idx),
|
||||
is_focused: Some(id) == focused_ws_id,
|
||||
active_window_id: ws.active_window().map(|win| win.id().get()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
events.push(Event::WorkspacesChanged { workspaces });
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn ipc_refresh_windows(&mut self) {
|
||||
let Some(server) = &self.niri.ipc_server else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _span = tracy_client::span!("State::ipc_refresh_windows");
|
||||
|
||||
let mut state = server.event_stream_state.borrow_mut();
|
||||
let state = &mut state.windows;
|
||||
|
||||
let mut events = Vec::new();
|
||||
let layout = &self.niri.layout;
|
||||
|
||||
// Check for window changes.
|
||||
let mut seen = HashSet::new();
|
||||
let mut focused_id = None;
|
||||
layout.with_windows(|mapped, _, ws_id| {
|
||||
let id = mapped.id().get();
|
||||
seen.insert(id);
|
||||
|
||||
if mapped.is_focused() {
|
||||
focused_id = Some(id);
|
||||
}
|
||||
|
||||
let Some(ipc_win) = state.windows.get(&id) else {
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace_id = ws_id.map(|id| id.get());
|
||||
let mut changed = ipc_win.workspace_id != workspace_id;
|
||||
|
||||
changed |= with_toplevel_role(mapped.toplevel(), |role| {
|
||||
ipc_win.title != role.title || ipc_win.app_id != role.app_id
|
||||
});
|
||||
|
||||
if changed {
|
||||
let window = make_ipc_window(mapped, ws_id);
|
||||
events.push(Event::WindowOpenedOrChanged { window });
|
||||
return;
|
||||
}
|
||||
|
||||
if mapped.is_focused() && !ipc_win.is_focused {
|
||||
events.push(Event::WindowFocusChanged { id: Some(id) });
|
||||
}
|
||||
});
|
||||
|
||||
// Check for closed windows.
|
||||
let mut ipc_focused_id = None;
|
||||
for (id, ipc_win) in &state.windows {
|
||||
if !seen.contains(id) {
|
||||
events.push(Event::WindowClosed { id: *id });
|
||||
}
|
||||
|
||||
if ipc_win.is_focused {
|
||||
ipc_focused_id = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Extra check for focus becoming None, since the checks above only work for focus becoming
|
||||
// a different window.
|
||||
if focused_id.is_none() && ipc_focused_id.is_some() {
|
||||
events.push(Event::WindowFocusChanged { id: None });
|
||||
}
|
||||
|
||||
for event in events {
|
||||
state.apply(event.clone());
|
||||
server.send_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform};
|
||||
use smithay::wayland::compositor::{Blocker, BlockerState};
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
@@ -21,6 +22,7 @@ use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
|
||||
use crate::utils::transaction::TransactionBlocker;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClosingWindow {
|
||||
@@ -46,7 +48,7 @@ pub struct ClosingWindow {
|
||||
blocked_out_buffer_offset: Point<f64, Logical>,
|
||||
|
||||
/// The closing animation.
|
||||
anim: Animation,
|
||||
anim_state: AnimationState,
|
||||
|
||||
/// Random seed for the shader.
|
||||
random_seed: f32,
|
||||
@@ -59,6 +61,29 @@ niri_render_elements! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AnimationState {
|
||||
Waiting {
|
||||
/// Blocker for a transaction before starting the animation.
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
},
|
||||
Animating(Animation),
|
||||
}
|
||||
|
||||
impl AnimationState {
|
||||
pub fn new(blocker: TransactionBlocker, anim: Animation) -> Self {
|
||||
if blocker.state() == BlockerState::Pending {
|
||||
Self::Waiting { blocker, anim }
|
||||
} else {
|
||||
// This actually doesn't normally happen because the window is removed only after the
|
||||
// closing animation is created. Though, it does happen with disable-transactions debug
|
||||
// flag.
|
||||
Self::Animating(anim)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClosingWindow {
|
||||
pub fn new<E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
@@ -66,6 +91,7 @@ impl ClosingWindow {
|
||||
scale: Scale<f64>,
|
||||
geo_size: Size<f64, Logical>,
|
||||
pos: Point<f64, Logical>,
|
||||
blocker: TransactionBlocker,
|
||||
anim: Animation,
|
||||
) -> anyhow::Result<Self> {
|
||||
let _span = tracy_client::span!("ClosingWindow::new");
|
||||
@@ -107,17 +133,29 @@ impl ClosingWindow {
|
||||
pos,
|
||||
buffer_offset,
|
||||
blocked_out_buffer_offset,
|
||||
anim,
|
||||
anim_state: AnimationState::new(blocker, anim),
|
||||
random_seed: fastrand::f32(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
self.anim.set_current_time(current_time);
|
||||
match &mut self.anim_state {
|
||||
AnimationState::Waiting { blocker, anim } => {
|
||||
if blocker.state() != BlockerState::Pending {
|
||||
let mut anim = anim.restarted(0., 1., 0.);
|
||||
anim.set_current_time(current_time);
|
||||
self.anim_state = AnimationState::Animating(anim);
|
||||
}
|
||||
}
|
||||
AnimationState::Animating(anim) => anim.set_current_time(current_time),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
!self.anim.is_done()
|
||||
match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => true,
|
||||
AnimationState::Animating(anim) => !anim.is_done(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
@@ -127,15 +165,42 @@ impl ClosingWindow {
|
||||
scale: Scale<f64>,
|
||||
target: RenderTarget,
|
||||
) -> ClosingWindowRenderElement {
|
||||
let progress = self.anim.value();
|
||||
let clamped_progress = self.anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
|
||||
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
|
||||
} else {
|
||||
(&self.buffer, self.buffer_offset)
|
||||
};
|
||||
|
||||
let anim = match &self.anim_state {
|
||||
AnimationState::Waiting { .. } => {
|
||||
let elem = TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
Point::from((0., 0.)),
|
||||
1.,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
);
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(elem);
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), 1.);
|
||||
|
||||
let mut location = self.pos + offset;
|
||||
location.x -= view_rect.loc.x;
|
||||
let elem = RelocateRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
);
|
||||
|
||||
return elem.into();
|
||||
}
|
||||
AnimationState::Animating(anim) => anim,
|
||||
};
|
||||
|
||||
let progress = anim.value();
|
||||
let clamped_progress = anim.clamped_value().clamp(0., 1.);
|
||||
|
||||
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
|
||||
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
|
||||
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::{CornerRadius, Gradient, GradientRelativeTo};
|
||||
use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
@@ -72,7 +72,7 @@ impl FocusRing {
|
||||
};
|
||||
|
||||
for buf in &mut self.buffers {
|
||||
buf.set_color(color.into());
|
||||
buf.set_color(color.to_array_premul());
|
||||
}
|
||||
|
||||
let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32);
|
||||
@@ -91,6 +91,7 @@ impl FocusRing {
|
||||
to: color,
|
||||
angle: 0,
|
||||
relative_to: GradientRelativeTo::Window,
|
||||
in_: GradientInterpolation::default(),
|
||||
});
|
||||
|
||||
let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size);
|
||||
@@ -178,8 +179,9 @@ impl FocusRing {
|
||||
border.update(
|
||||
size,
|
||||
Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size),
|
||||
gradient.from.into(),
|
||||
gradient.to.into(),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
@@ -198,8 +200,9 @@ impl FocusRing {
|
||||
gradient_area.loc - self.locations[0],
|
||||
gradient_area.size,
|
||||
),
|
||||
gradient.from.into(),
|
||||
gradient.to.into(),
|
||||
gradient.in_,
|
||||
gradient.from,
|
||||
gradient.to,
|
||||
((gradient.angle as f32) - 90.).to_radians(),
|
||||
Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size),
|
||||
rounded_corner_border_width,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use niri_config::{CornerRadius, FloatOrInt};
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
|
||||
use super::focus_ring::{FocusRing, FocusRingRenderElement};
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InsertHintElement {
|
||||
inner: FocusRing,
|
||||
}
|
||||
|
||||
pub type InsertHintRenderElement = FocusRingRenderElement;
|
||||
|
||||
impl InsertHintElement {
|
||||
pub fn new(config: niri_config::InsertHint) -> Self {
|
||||
Self {
|
||||
inner: FocusRing::new(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: niri_config::InsertHint) {
|
||||
self.inner.update_config(niri_config::FocusRing {
|
||||
off: config.off,
|
||||
width: FloatOrInt(0.),
|
||||
active_color: config.color,
|
||||
inactive_color: config.color,
|
||||
active_gradient: config.gradient,
|
||||
inactive_gradient: config.gradient,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.inner.update_shaders();
|
||||
}
|
||||
|
||||
pub fn update_render_elements(
|
||||
&mut self,
|
||||
size: Size<f64, Logical>,
|
||||
view_rect: Rectangle<f64, Logical>,
|
||||
radius: CornerRadius,
|
||||
scale: f64,
|
||||
) {
|
||||
self.inner
|
||||
.update_render_elements(size, true, false, view_rect, radius, scale);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
renderer: &mut impl NiriRenderer,
|
||||
location: Point<f64, Logical>,
|
||||
) -> impl Iterator<Item = FocusRingRenderElement> {
|
||||
self.inner.render(renderer, location)
|
||||
}
|
||||
}
|
||||
+1957
-296
File diff suppressed because it is too large
Load Diff
+265
-240
@@ -9,6 +9,7 @@ use smithay::backend::renderer::element::utils::{
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Rectangle};
|
||||
|
||||
use super::tile::Tile;
|
||||
use super::workspace::{
|
||||
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
|
||||
WorkspaceRenderElement,
|
||||
@@ -19,7 +20,8 @@ use crate::input::swipe_tracker::SwipeTracker;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::utils::{output_size, to_physical_precise_round, ResizeEdge};
|
||||
use crate::utils::transaction::Transaction;
|
||||
use crate::utils::{output_size, round_logical_in_physical, ResizeEdge};
|
||||
|
||||
/// Amount of touchpad movement to scroll the height of one workspace.
|
||||
const WORKSPACE_GESTURE_MOVEMENT: f64 = 300.;
|
||||
@@ -32,17 +34,19 @@ const WORKSPACE_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
|
||||
#[derive(Debug)]
|
||||
pub struct Monitor<W: LayoutElement> {
|
||||
/// Output for this monitor.
|
||||
pub output: Output,
|
||||
pub(super) output: Output,
|
||||
/// Cached name of the output.
|
||||
output_name: String,
|
||||
// Must always contain at least one.
|
||||
pub workspaces: Vec<Workspace<W>>,
|
||||
pub(super) workspaces: Vec<Workspace<W>>,
|
||||
/// Index of the currently active workspace.
|
||||
pub active_workspace_idx: usize,
|
||||
pub(super) active_workspace_idx: usize,
|
||||
/// ID of the previously active workspace.
|
||||
pub previous_workspace_id: Option<WorkspaceId>,
|
||||
pub(super) previous_workspace_id: Option<WorkspaceId>,
|
||||
/// In-progress switch between workspaces.
|
||||
pub workspace_switch: Option<WorkspaceSwitch>,
|
||||
pub(super) workspace_switch: Option<WorkspaceSwitch>,
|
||||
/// Configurable properties of the layout.
|
||||
pub options: Rc<Options>,
|
||||
pub(super) options: Rc<Options>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -56,7 +60,7 @@ pub struct WorkspaceSwitchGesture {
|
||||
/// Index of the workspace where the gesture was started.
|
||||
center_idx: usize,
|
||||
/// Current, fractional workspace index.
|
||||
pub current_idx: f64,
|
||||
pub(super) current_idx: f64,
|
||||
tracker: SwipeTracker,
|
||||
/// Whether the gesture is controlled by the touchpad.
|
||||
is_touchpad: bool,
|
||||
@@ -92,6 +96,7 @@ impl WorkspaceSwitch {
|
||||
impl<W: LayoutElement> Monitor<W> {
|
||||
pub fn new(output: Output, workspaces: Vec<Workspace<W>>, options: Rc<Options>) -> Self {
|
||||
Self {
|
||||
output_name: output.name(),
|
||||
output,
|
||||
workspaces,
|
||||
active_workspace_idx: 0,
|
||||
@@ -101,6 +106,18 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output(&self) -> &Output {
|
||||
&self.output
|
||||
}
|
||||
|
||||
pub fn output_name(&self) -> &String {
|
||||
&self.output_name
|
||||
}
|
||||
|
||||
pub fn active_workspace_idx(&self) -> usize {
|
||||
self.active_workspace_idx
|
||||
}
|
||||
|
||||
pub fn active_workspace_ref(&self) -> &Workspace<W> {
|
||||
&self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
@@ -125,6 +142,14 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
&mut self.workspaces[self.active_workspace_idx]
|
||||
}
|
||||
|
||||
pub fn windows(&self) -> impl Iterator<Item = &W> {
|
||||
self.workspaces.iter().flat_map(|ws| ws.windows())
|
||||
}
|
||||
|
||||
pub fn has_window(&self, window: &W::Id) -> bool {
|
||||
self.windows().any(|win| win.id() == window)
|
||||
}
|
||||
|
||||
fn activate_workspace(&mut self, idx: usize) {
|
||||
if self.active_workspace_idx == idx {
|
||||
return;
|
||||
@@ -159,7 +184,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_window(window, activate, width, is_full_width);
|
||||
workspace.add_window(None, window, activate, width, is_full_width);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
@@ -193,12 +218,15 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
|
||||
// Since we're adding window right of something, the workspace isn't empty, and therefore
|
||||
// cannot be the last one, so we never need to insert a new empty workspace.
|
||||
}
|
||||
|
||||
pub fn add_column(&mut self, workspace_idx: usize, column: Column<W>, activate: bool) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_column(column, activate);
|
||||
workspace.add_column(None, column, activate, None);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
@@ -214,6 +242,56 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tile(
|
||||
&mut self,
|
||||
workspace_idx: usize,
|
||||
column_idx: Option<usize>,
|
||||
tile: Tile<W>,
|
||||
activate: bool,
|
||||
width: ColumnWidth,
|
||||
is_full_width: bool,
|
||||
) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_tile(column_idx, tile, activate, width, is_full_width, None);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
|
||||
if workspace_idx == self.workspaces.len() - 1 {
|
||||
// Insert a new empty workspace.
|
||||
let ws = Workspace::new(self.output.clone(), self.options.clone());
|
||||
self.workspaces.push(ws);
|
||||
}
|
||||
|
||||
if activate {
|
||||
self.activate_workspace(workspace_idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tile_to_column(
|
||||
&mut self,
|
||||
workspace_idx: usize,
|
||||
column_idx: usize,
|
||||
tile_idx: Option<usize>,
|
||||
tile: Tile<W>,
|
||||
activate: bool,
|
||||
) {
|
||||
let workspace = &mut self.workspaces[workspace_idx];
|
||||
|
||||
workspace.add_tile_to_column(column_idx, tile_idx, tile, activate);
|
||||
|
||||
// After adding a new window, workspace becomes this output's own.
|
||||
workspace.original_output = OutputId::new(&self.output);
|
||||
|
||||
// Since we're adding window to an existing column, the workspace isn't empty, and
|
||||
// therefore cannot be the last one, so we never need to insert a new empty workspace.
|
||||
|
||||
if activate {
|
||||
self.activate_workspace(workspace_idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_up_workspaces(&mut self) {
|
||||
assert!(self.workspace_switch.is_none());
|
||||
|
||||
@@ -298,14 +376,6 @@ 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) {
|
||||
self.active_workspace().focus_left();
|
||||
}
|
||||
@@ -387,7 +457,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let curr_idx = workspace.columns[workspace.active_column_idx].active_tile_idx;
|
||||
let new_idx = curr_idx.saturating_sub(1);
|
||||
if curr_idx == new_idx {
|
||||
self.focus_left();
|
||||
self.focus_right();
|
||||
} else {
|
||||
workspace.focus_up();
|
||||
}
|
||||
@@ -439,13 +509,20 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
|
||||
let column = &workspace.columns[workspace.active_column_idx];
|
||||
let width = column.width;
|
||||
let is_full_width = column.is_full_width;
|
||||
let window = workspace
|
||||
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
|
||||
.into_window();
|
||||
let removed = workspace.remove_tile_by_idx(
|
||||
workspace.active_column_idx,
|
||||
column.active_tile_idx,
|
||||
Transaction::new(),
|
||||
None,
|
||||
);
|
||||
|
||||
self.add_window(new_idx, window, true, width, is_full_width);
|
||||
self.add_window(
|
||||
new_idx,
|
||||
removed.tile.into_window(),
|
||||
true,
|
||||
removed.width,
|
||||
removed.is_full_width,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn move_to_workspace_down(&mut self) {
|
||||
@@ -462,17 +539,48 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
|
||||
let column = &workspace.columns[workspace.active_column_idx];
|
||||
let width = column.width;
|
||||
let is_full_width = column.is_full_width;
|
||||
let window = workspace
|
||||
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
|
||||
.into_window();
|
||||
let removed = workspace.remove_tile_by_idx(
|
||||
workspace.active_column_idx,
|
||||
column.active_tile_idx,
|
||||
Transaction::new(),
|
||||
None,
|
||||
);
|
||||
|
||||
self.add_window(new_idx, window, true, width, is_full_width);
|
||||
self.add_window(
|
||||
new_idx,
|
||||
removed.tile.into_window(),
|
||||
true,
|
||||
removed.width,
|
||||
removed.is_full_width,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn move_to_workspace(&mut self, idx: usize) {
|
||||
let source_workspace_idx = self.active_workspace_idx;
|
||||
pub fn move_to_workspace(&mut self, window: Option<&W::Id>, idx: usize) {
|
||||
let (source_workspace_idx, col_idx, tile_idx) = if let Some(window) = window {
|
||||
self.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(ws_idx, ws)| {
|
||||
ws.columns.iter().enumerate().find_map(|(col_idx, col)| {
|
||||
col.tiles
|
||||
.iter()
|
||||
.position(|tile| tile.window().id() == window)
|
||||
.map(|tile_idx| (ws_idx, col_idx, tile_idx))
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
} else {
|
||||
let ws_idx = self.active_workspace_idx;
|
||||
|
||||
let ws = &self.workspaces[ws_idx];
|
||||
if ws.columns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let col_idx = ws.active_column_idx;
|
||||
let tile_idx = ws.columns[col_idx].active_tile_idx;
|
||||
(ws_idx, col_idx, tile_idx)
|
||||
};
|
||||
|
||||
let new_idx = min(idx, self.workspaces.len() - 1);
|
||||
if new_idx == source_workspace_idx {
|
||||
@@ -480,23 +588,24 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
|
||||
let workspace = &mut self.workspaces[source_workspace_idx];
|
||||
if workspace.columns.is_empty() {
|
||||
return;
|
||||
let column = &workspace.columns[col_idx];
|
||||
let activate = source_workspace_idx == self.active_workspace_idx
|
||||
&& col_idx == workspace.active_column_idx
|
||||
&& tile_idx == column.active_tile_idx;
|
||||
|
||||
let removed = workspace.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
|
||||
|
||||
self.add_window(
|
||||
new_idx,
|
||||
removed.tile.into_window(),
|
||||
activate,
|
||||
removed.width,
|
||||
removed.is_full_width,
|
||||
);
|
||||
|
||||
if self.workspace_switch.is_none() {
|
||||
self.clean_up_workspaces();
|
||||
}
|
||||
|
||||
let column = &workspace.columns[workspace.active_column_idx];
|
||||
let width = column.width;
|
||||
let is_full_width = column.is_full_width;
|
||||
let window = workspace
|
||||
.remove_tile_by_idx(workspace.active_column_idx, column.active_tile_idx, None)
|
||||
.into_window();
|
||||
|
||||
self.add_window(new_idx, window, true, width, is_full_width);
|
||||
|
||||
// Don't animate this action.
|
||||
self.workspace_switch = None;
|
||||
|
||||
self.clean_up_workspaces();
|
||||
}
|
||||
|
||||
pub fn move_column_to_workspace_up(&mut self) {
|
||||
@@ -512,7 +621,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
|
||||
self.add_column(new_idx, column, true);
|
||||
}
|
||||
|
||||
@@ -529,7 +638,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
|
||||
self.add_column(new_idx, column, true);
|
||||
}
|
||||
|
||||
@@ -546,13 +655,8 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx);
|
||||
let column = workspace.remove_column_by_idx(workspace.active_column_idx, None);
|
||||
self.add_column(new_idx, column, true);
|
||||
|
||||
// Don't animate this action.
|
||||
self.workspace_switch = None;
|
||||
|
||||
self.clean_up_workspaces();
|
||||
}
|
||||
|
||||
pub fn switch_workspace_up(&mut self) {
|
||||
@@ -571,13 +675,8 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.workspaces.iter().position(|w| w.id() == id)
|
||||
}
|
||||
|
||||
pub fn switch_workspace(&mut self, idx: usize, animate: bool) {
|
||||
pub fn switch_workspace(&mut self, idx: usize) {
|
||||
self.activate_workspace(min(idx, self.workspaces.len() - 1));
|
||||
|
||||
if !animate {
|
||||
self.workspace_switch = None;
|
||||
self.clean_up_workspaces();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_workspace_auto_back_and_forth(&mut self, idx: usize) {
|
||||
@@ -585,16 +684,16 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
if idx == self.active_workspace_idx {
|
||||
if let Some(prev_idx) = self.previous_workspace_idx() {
|
||||
self.switch_workspace(prev_idx, false);
|
||||
self.switch_workspace(prev_idx);
|
||||
}
|
||||
} else {
|
||||
self.switch_workspace(idx, false);
|
||||
self.switch_workspace(idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_workspace_previous(&mut self) {
|
||||
if let Some(idx) = self.previous_workspace_idx() {
|
||||
self.switch_workspace(idx, false);
|
||||
self.switch_workspace(idx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,7 +733,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
pub(super) fn are_animations_ongoing(&self) -> bool {
|
||||
self.workspace_switch
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.is_animation())
|
||||
@@ -709,14 +808,6 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.active_workspace().set_column_width(change);
|
||||
}
|
||||
|
||||
pub fn set_window_height(&mut self, change: SizeChange) {
|
||||
self.active_workspace().set_window_height(change);
|
||||
}
|
||||
|
||||
pub fn reset_window_height(&mut self) {
|
||||
self.active_workspace().reset_window_height();
|
||||
}
|
||||
|
||||
pub fn move_workspace_down(&mut self) {
|
||||
let new_idx = min(self.active_workspace_idx + 1, self.workspaces.len() - 1);
|
||||
if new_idx == self.active_workspace_idx {
|
||||
@@ -780,90 +871,75 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
Some(rect)
|
||||
}
|
||||
|
||||
pub fn workspaces_with_render_positions(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&Workspace<W>, Point<f64, Logical>)> {
|
||||
let mut first = None;
|
||||
let mut second = None;
|
||||
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
if after_idx >= 0. && before_idx < self.workspaces.len() as f64 {
|
||||
let scale = self.output.current_scale().fractional_scale();
|
||||
let size = output_size(&self.output);
|
||||
let offset =
|
||||
round_logical_in_physical(scale, (render_idx - before_idx) * size.h);
|
||||
|
||||
// Ceil the height in physical pixels.
|
||||
let height = (size.h * scale).ceil() / scale;
|
||||
|
||||
if before_idx >= 0. {
|
||||
let before_idx = before_idx as usize;
|
||||
let before_offset = Point::from((0., -offset));
|
||||
first = Some((&self.workspaces[before_idx], before_offset));
|
||||
}
|
||||
|
||||
let after_idx = after_idx as usize;
|
||||
if after_idx < self.workspaces.len() {
|
||||
let after_offset = Point::from((0., -offset + height));
|
||||
second = Some((&self.workspaces[after_idx], after_offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
first = Some((
|
||||
&self.workspaces[self.active_workspace_idx],
|
||||
Point::from((0., 0.)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
first.into_iter().chain(second)
|
||||
}
|
||||
|
||||
pub fn workspace_under(
|
||||
&self,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> Option<(&Workspace<W>, Point<f64, Logical>)> {
|
||||
let size = output_size(&self.output);
|
||||
let (ws, bounds) = self
|
||||
.workspaces_with_render_positions()
|
||||
.map(|(ws, offset)| (ws, Rectangle::from_loc_and_size(offset, size)))
|
||||
.find(|(_, bounds)| bounds.contains(pos_within_output))?;
|
||||
Some((ws, bounds.loc))
|
||||
}
|
||||
|
||||
pub fn window_under(
|
||||
&self,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> Option<(&W, Option<Point<f64, Logical>>)> {
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let size = output_size(&self.output).to_f64();
|
||||
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
let offset = (render_idx - before_idx) * size.h;
|
||||
|
||||
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 {
|
||||
if before_idx < 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
(before_idx as usize, Point::from((0., offset)))
|
||||
} else {
|
||||
if after_idx >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
(after_idx, Point::from((0., -size.h + offset)))
|
||||
};
|
||||
|
||||
let ws = &self.workspaces[idx];
|
||||
let (win, win_pos) = ws.window_under(pos_within_output + ws_offset)?;
|
||||
Some((win, win_pos.map(|p| p - ws_offset)))
|
||||
}
|
||||
None => {
|
||||
let ws = &self.workspaces[self.active_workspace_idx];
|
||||
ws.window_under(pos_within_output)
|
||||
}
|
||||
}
|
||||
let (ws, offset) = self.workspace_under(pos_within_output)?;
|
||||
let (win, win_pos) = ws.window_under(pos_within_output - offset)?;
|
||||
Some((win, win_pos.map(|p| p + offset)))
|
||||
}
|
||||
|
||||
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let size = output_size(&self.output);
|
||||
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
|
||||
let offset = (render_idx - before_idx) * size.h;
|
||||
|
||||
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 {
|
||||
if before_idx < 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
(before_idx as usize, Point::from((0., offset)))
|
||||
} else {
|
||||
if after_idx >= self.workspaces.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
(after_idx, Point::from((0., -size.h + offset)))
|
||||
};
|
||||
|
||||
let ws = &self.workspaces[idx];
|
||||
ws.resize_edges_under(pos_within_output + ws_offset)
|
||||
}
|
||||
None => {
|
||||
let ws = &self.workspaces[self.active_workspace_idx];
|
||||
ws.resize_edges_under(pos_within_output)
|
||||
}
|
||||
}
|
||||
let (ws, offset) = self.workspace_under(pos_within_output)?;
|
||||
ws.resize_edges_under(pos_within_output - offset)
|
||||
}
|
||||
|
||||
pub fn render_above_top_layer(&self) -> bool {
|
||||
@@ -876,103 +952,52 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
ws.render_above_top_layer()
|
||||
}
|
||||
|
||||
pub fn render_elements<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
pub fn render_elements<'a, R: NiriRenderer>(
|
||||
&'a self,
|
||||
renderer: &'a mut R,
|
||||
target: RenderTarget,
|
||||
) -> Vec<MonitorRenderElement<R>> {
|
||||
) -> impl Iterator<Item = MonitorRenderElement<R>> + '_ {
|
||||
let _span = tracy_client::span!("Monitor::render_elements");
|
||||
|
||||
let scale = self.output.current_scale().fractional_scale();
|
||||
let size = output_size(&self.output);
|
||||
// Ceil the height in physical pixels.
|
||||
let height = (size.h * scale).ceil() as i32;
|
||||
|
||||
match &self.workspace_switch {
|
||||
Some(switch) => {
|
||||
let render_idx = switch.current_idx();
|
||||
let before_idx = render_idx.floor();
|
||||
let after_idx = render_idx.ceil();
|
||||
// Crop the elements to prevent them overflowing, currently visible during a workspace
|
||||
// switch.
|
||||
//
|
||||
// HACK: crop to infinite bounds at least horizontally where we
|
||||
// know there's no workspace joining or monitor bounds, otherwise
|
||||
// it will cut pixel shaders and mess up the coordinate space.
|
||||
// There's also a damage tracking bug which causes glitched
|
||||
// rendering for maximized GTK windows.
|
||||
//
|
||||
// FIXME: use proper bounds after fixing the Crop element.
|
||||
let crop_bounds = if self.workspace_switch.is_some() {
|
||||
Rectangle::from_loc_and_size((-i32::MAX / 2, 0), (i32::MAX, height))
|
||||
} else {
|
||||
Rectangle::from_loc_and_size((-i32::MAX / 2, -i32::MAX / 2), (i32::MAX, i32::MAX))
|
||||
};
|
||||
|
||||
let offset = (render_idx - before_idx) * size.h;
|
||||
|
||||
if after_idx < 0. || before_idx as usize >= self.workspaces.len() {
|
||||
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,
|
||||
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),
|
||||
),
|
||||
)?,
|
||||
Point::from((0., -offset + size.h)).to_physical_precise_round(scale),
|
||||
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| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
scale,
|
||||
Rectangle::from_extemities(
|
||||
(-i32::MAX / 2, -i32::MAX / 2),
|
||||
(i32::MAX / 2, to_physical_precise_round(scale, size.h)),
|
||||
),
|
||||
)?,
|
||||
Point::from((0., -offset)).to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
))
|
||||
});
|
||||
before.chain(after.into_iter().flatten()).collect()
|
||||
}
|
||||
None => {
|
||||
let elements =
|
||||
self.workspaces[self.active_workspace_idx].render_elements(renderer, target);
|
||||
elements
|
||||
self.workspaces_with_render_positions()
|
||||
.flat_map(move |(ws, offset)| {
|
||||
ws.render_elements(renderer, target)
|
||||
.into_iter()
|
||||
.filter_map(|elem| {
|
||||
Some(RelocateRenderElement::from_element(
|
||||
CropRenderElement::from_element(
|
||||
elem,
|
||||
scale,
|
||||
// HACK: set infinite crop bounds due to a damage tracking bug
|
||||
// which causes glitched rendering for maximized GTK windows.
|
||||
// FIXME: use proper bounds after fixing the Crop element.
|
||||
Rectangle::from_loc_and_size(
|
||||
(-i32::MAX / 2, -i32::MAX / 2),
|
||||
(i32::MAX, i32::MAX),
|
||||
),
|
||||
// Rectangle::from_loc_and_size((0, 0), size),
|
||||
)?,
|
||||
(0, 0),
|
||||
Relocate::Relative,
|
||||
))
|
||||
.filter_map(move |elem| {
|
||||
CropRenderElement::from_element(elem, scale, crop_bounds)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
.map(move |elem| {
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_positions() is already
|
||||
// rounded to physical pixels, but it's in the logical coordinate
|
||||
// space, so we need to convert it to physical.
|
||||
offset.to_physical_precise_round(scale),
|
||||
Relocate::Relative,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn workspace_switch_gesture_begin(&mut self, is_touchpad: bool) {
|
||||
|
||||
+27
-8
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::CornerRadius;
|
||||
use niri_config::{Color, CornerRadius, GradientInterpolation};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::{Element, Kind};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
@@ -23,6 +23,7 @@ use crate::render_helpers::resize::ResizeRenderElement;
|
||||
use crate::render_helpers::snapshot::RenderSnapshot;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
|
||||
use crate::utils::transaction::Transaction;
|
||||
|
||||
/// Toplevel window with decorations.
|
||||
#[derive(Debug)]
|
||||
@@ -63,6 +64,9 @@ pub struct Tile<W: LayoutElement> {
|
||||
/// The animation of a tile visually moving vertically.
|
||||
move_y_animation: Option<MoveAnimation>,
|
||||
|
||||
/// Offset during the initial interactive move rubberband.
|
||||
pub(super) interactive_move_offset: Point<f64, Logical>,
|
||||
|
||||
/// Snapshot of the last render for use in the close animation.
|
||||
unmap_snapshot: Option<TileRenderSnapshot>,
|
||||
|
||||
@@ -73,7 +77,7 @@ pub struct Tile<W: LayoutElement> {
|
||||
scale: f64,
|
||||
|
||||
/// Configurable properties of the layout.
|
||||
pub options: Rc<Options>,
|
||||
pub(super) options: Rc<Options>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
@@ -89,7 +93,7 @@ niri_render_elements! {
|
||||
}
|
||||
}
|
||||
|
||||
type TileRenderSnapshot =
|
||||
pub type TileRenderSnapshot =
|
||||
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -122,6 +126,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
resize_animation: None,
|
||||
move_x_animation: None,
|
||||
move_y_animation: None,
|
||||
interactive_move_offset: Point::from((0., 0.)),
|
||||
unmap_snapshot: None,
|
||||
rounded_corner_damage: Default::default(),
|
||||
scale,
|
||||
@@ -304,6 +309,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
offset.y += move_.from * move_.anim.value();
|
||||
}
|
||||
|
||||
offset += self.interactive_move_offset;
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
@@ -363,6 +370,11 @@ impl<W: LayoutElement> Tile<W> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stop_move_animations(&mut self) {
|
||||
self.move_x_animation = None;
|
||||
self.move_y_animation = None;
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &W {
|
||||
&self.window
|
||||
}
|
||||
@@ -380,7 +392,7 @@ impl<W: LayoutElement> Tile<W> {
|
||||
}
|
||||
|
||||
/// Returns `None` if the border is hidden and `Some(width)` if it should be shown.
|
||||
fn effective_border_width(&self) -> Option<f64> {
|
||||
pub fn effective_border_width(&self) -> Option<f64> {
|
||||
if self.is_fullscreen {
|
||||
return None;
|
||||
}
|
||||
@@ -503,7 +515,12 @@ impl<W: LayoutElement> Tile<W> {
|
||||
activation_region.contains(point)
|
||||
}
|
||||
|
||||
pub fn request_tile_size(&mut self, mut size: Size<f64, Logical>, animate: bool) {
|
||||
pub fn request_tile_size(
|
||||
&mut self,
|
||||
mut size: Size<f64, Logical>,
|
||||
animate: bool,
|
||||
transaction: Option<Transaction>,
|
||||
) {
|
||||
// Can't go through effective_border_width() because we might be fullscreen.
|
||||
if !self.border.is_off() {
|
||||
let width = self.border.width();
|
||||
@@ -514,7 +531,8 @@ impl<W: LayoutElement> Tile<W> {
|
||||
// The size request has to be i32 unfortunately, due to Wayland. We floor here instead of
|
||||
// round to avoid situations where proportionally-sized columns don't fit on the screen
|
||||
// exactly.
|
||||
self.window.request_size(size.to_i32_floor(), animate);
|
||||
self.window
|
||||
.request_size(size.to_i32_floor(), animate, transaction);
|
||||
}
|
||||
|
||||
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
|
||||
@@ -757,8 +775,9 @@ impl<W: LayoutElement> Tile<W> {
|
||||
return BorderRenderElement::new(
|
||||
geo.size,
|
||||
Rectangle::from_loc_and_size((0., 0.), geo.size),
|
||||
elem.color(),
|
||||
elem.color(),
|
||||
GradientInterpolation::default(),
|
||||
Color::from_color32f(elem.color()),
|
||||
Color::from_color32f(elem.color()),
|
||||
0.,
|
||||
Rectangle::from_loc_and_size((0., 0.), geo.size),
|
||||
0.,
|
||||
|
||||
+1303
-635
File diff suppressed because it is too large
Load Diff
+85
-61
@@ -24,6 +24,7 @@ use niri::utils::spawning::{
|
||||
use niri::utils::watcher::Watcher;
|
||||
use niri::utils::{cause_panic, version, IS_SYSTEMD_SERVICE};
|
||||
use niri_config::Config;
|
||||
use niri_ipc::socket::SOCKET_PATH_ENV;
|
||||
use portable_atomic::Ordering;
|
||||
use sd_notify::NotifyState;
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
@@ -32,6 +33,11 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
const DEFAULT_LOG_FILTER: &str = "niri=debug,smithay::backend::renderer::gles=error";
|
||||
|
||||
#[cfg(feature = "profile-with-tracy-allocations")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||
tracy_client::ProfiledAllocator::new(std::alloc::System, 100);
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set backtrace defaults if not set.
|
||||
if env::var_os("RUST_BACKTRACE").is_none() {
|
||||
@@ -90,10 +96,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Sub::Validate { config } => {
|
||||
tracy_client::Client::start();
|
||||
|
||||
let path = config
|
||||
.or_else(env_config_path)
|
||||
.or_else(default_config_path)
|
||||
.expect("error getting config path");
|
||||
let (path, _, _) = config_path(config);
|
||||
Config::load(&path)?;
|
||||
info!("config is valid");
|
||||
return Ok(());
|
||||
@@ -114,56 +117,50 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Load the config.
|
||||
let mut config_created = false;
|
||||
let path = cli.config.or_else(env_config_path);
|
||||
let (path, watch_path, create_default) = config_path(cli.config);
|
||||
env::remove_var("NIRI_CONFIG");
|
||||
let path = path.or_else(|| {
|
||||
let default_path = default_config_path()?;
|
||||
let default_parent = default_path.parent().unwrap();
|
||||
if create_default {
|
||||
let default_parent = 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)
|
||||
match fs::create_dir_all(default_parent) {
|
||||
Ok(()) => {
|
||||
// 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(&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 {:?}", &path);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error writing config file at {:?}: {err:?}", &path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => warn!("error creating config file at {:?}: {err:?}", &path),
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => warn!("error creating config file at {:?}: {err:?}", &default_path),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"error creating config directories {:?}: {err:?}",
|
||||
default_parent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some(default_path)
|
||||
});
|
||||
}
|
||||
|
||||
let mut config_errored = false;
|
||||
let mut config = path
|
||||
.as_deref()
|
||||
.and_then(|path| match Config::load(path) {
|
||||
Ok(config) => Some(config),
|
||||
Err(err) => {
|
||||
warn!("{err:?}");
|
||||
config_errored = true;
|
||||
None
|
||||
}
|
||||
let mut config = Config::load(&path)
|
||||
.map_err(|err| {
|
||||
warn!("{err:?}");
|
||||
config_errored = true;
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -200,7 +197,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Set NIRI_SOCKET for children.
|
||||
if let Some(ipc) = &state.niri.ipc_server {
|
||||
env::set_var(niri_ipc::SOCKET_PATH_ENV, &ipc.socket_path);
|
||||
env::set_var(SOCKET_PATH_ENV, &ipc.socket_path);
|
||||
info!("IPC listening on: {}", ipc.socket_path.to_string_lossy());
|
||||
}
|
||||
|
||||
@@ -220,30 +217,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "dbus")]
|
||||
dbus::DBusServers::start(&mut state, cli.session);
|
||||
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
warn!("error notifying systemd: {err:?}");
|
||||
};
|
||||
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").map_or(true, |x| x != "1") {
|
||||
// Notify systemd we're ready.
|
||||
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
|
||||
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:?}");
|
||||
// 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.
|
||||
let _watcher = if let Some(path) = path.clone() {
|
||||
let _watcher = {
|
||||
let (tx, rx) = calloop::channel::sync_channel(1);
|
||||
let watcher = Watcher::new(path.clone(), tx);
|
||||
let watcher = Watcher::new(watch_path.clone(), tx);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(rx, move |event, _, state| match event {
|
||||
calloop::channel::Event::Msg(()) => state.reload_config(path.clone()),
|
||||
calloop::channel::Event::Msg(()) => state.reload_config(watch_path.clone()),
|
||||
calloop::channel::Event::Closed => (),
|
||||
})
|
||||
.unwrap();
|
||||
Some(watcher)
|
||||
} else {
|
||||
None
|
||||
watcher
|
||||
};
|
||||
|
||||
// Spawn commands from cli and auto-start.
|
||||
@@ -273,7 +270,7 @@ fn import_environment() {
|
||||
"WAYLAND_DISPLAY",
|
||||
"XDG_CURRENT_DESKTOP",
|
||||
"XDG_SESSION_TYPE",
|
||||
niri_ipc::SOCKET_PATH_ENV,
|
||||
SOCKET_PATH_ENV,
|
||||
]
|
||||
.join(" ");
|
||||
|
||||
@@ -335,6 +332,33 @@ fn default_config_path() -> Option<PathBuf> {
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn system_config_path() -> PathBuf {
|
||||
PathBuf::from("/etc/niri/config.kdl")
|
||||
}
|
||||
|
||||
/// Resolves and returns the config path to load, the config path to watch, and whether to create
|
||||
/// the default config at the path to load.
|
||||
fn config_path(cli_path: Option<PathBuf>) -> (PathBuf, PathBuf, bool) {
|
||||
if let Some(explicit) = cli_path.or_else(env_config_path) {
|
||||
return (explicit.clone(), explicit, false);
|
||||
}
|
||||
|
||||
let system_path = system_config_path();
|
||||
if let Some(path) = default_config_path() {
|
||||
if path.exists() {
|
||||
return (path.clone(), path, true);
|
||||
}
|
||||
|
||||
if system_path.exists() {
|
||||
(system_path, path, false)
|
||||
} else {
|
||||
(path.clone(), path, true)
|
||||
}
|
||||
} else {
|
||||
(system_path.clone(), system_path, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_fd() -> anyhow::Result<()> {
|
||||
let fd = match env::var("NOTIFY_FD") {
|
||||
Ok(notify_fd) => notify_fd.parse()?,
|
||||
|
||||
+892
-343
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,7 @@ 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 smithay::wayland::shell::xdg::{ToplevelStateSet, XdgToplevelSurfaceRoleAttributes};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
|
||||
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
|
||||
};
|
||||
@@ -22,6 +19,7 @@ use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
|
||||
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
use crate::niri::State;
|
||||
use crate::utils::with_toplevel_role;
|
||||
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
@@ -95,38 +93,24 @@ pub fn refresh(state: &mut State) {
|
||||
// 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();
|
||||
|
||||
state.niri.layout.with_windows(|mapped, output, _| {
|
||||
let toplevel = mapped.toplevel();
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role(toplevel, |role| {
|
||||
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);
|
||||
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);
|
||||
let toplevel = window.toplevel().expect("no X11 support");
|
||||
let wl_surface = toplevel.wl_surface();
|
||||
with_toplevel_role(toplevel, |role| {
|
||||
refresh_toplevel(protocol_state, wl_surface, role, output.as_ref(), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
pub mod foreign_toplevel;
|
||||
pub mod gamma_control;
|
||||
pub mod mutter_x11_interop;
|
||||
pub mod output_management;
|
||||
pub mod screencopy;
|
||||
|
||||
pub mod raw;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use mutter_x11_interop::MutterX11Interop;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
|
||||
use super::raw::mutter_x11_interop::v1::server::mutter_x11_interop;
|
||||
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
pub struct MutterX11InteropManagerState {}
|
||||
|
||||
pub struct MutterX11InteropManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
pub trait MutterX11InteropHandler {}
|
||||
|
||||
impl MutterX11InteropManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
|
||||
D: Dispatch<MutterX11Interop, ()>,
|
||||
D: MutterX11InteropHandler,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = MutterX11InteropManagerGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, MutterX11Interop, _>(VERSION, global_data);
|
||||
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData, D>
|
||||
for MutterX11InteropManagerState
|
||||
where
|
||||
D: GlobalDispatch<MutterX11Interop, MutterX11InteropManagerGlobalData>,
|
||||
D: Dispatch<MutterX11Interop, ()>,
|
||||
D: MutterX11InteropHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
_state: &mut D,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
manager: New<MutterX11Interop>,
|
||||
_manager_state: &MutterX11InteropManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
data_init.init(manager, ());
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &MutterX11InteropManagerGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<MutterX11Interop, (), D> for MutterX11InteropManagerState
|
||||
where
|
||||
D: Dispatch<MutterX11Interop, ()>,
|
||||
D: MutterX11InteropHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_resource: &MutterX11Interop,
|
||||
request: <MutterX11Interop as Resource>::Request,
|
||||
_data: &(),
|
||||
_dhandle: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
mutter_x11_interop::Request::Destroy => (),
|
||||
mutter_x11_interop::Request::SetX11Parent { .. } => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_mutter_x11_interop {
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: $crate::protocols::mutter_x11_interop::MutterX11InteropManagerGlobalData
|
||||
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
|
||||
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
$crate::protocols::raw::mutter_x11_interop::v1::server::mutter_x11_interop::MutterX11Interop: ()
|
||||
] => $crate::protocols::mutter_x11_interop::MutterX11InteropManagerState);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,897 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
|
||||
use niri_config::{FloatOrInt, OutputName, Vrr};
|
||||
use niri_ipc::Transform;
|
||||
use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{
|
||||
zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1,
|
||||
zwlr_output_manager_v1, zwlr_output_mode_v1,
|
||||
};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::Transform as WlTransform;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
|
||||
};
|
||||
use zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1;
|
||||
use zwlr_output_configuration_v1::ZwlrOutputConfigurationV1;
|
||||
use zwlr_output_head_v1::{AdaptiveSyncState, ZwlrOutputHeadV1};
|
||||
use zwlr_output_manager_v1::ZwlrOutputManagerV1;
|
||||
use zwlr_output_mode_v1::ZwlrOutputModeV1;
|
||||
|
||||
use crate::backend::OutputId;
|
||||
use crate::niri::State;
|
||||
use crate::utils::ipc_transform_to_smithay;
|
||||
|
||||
const VERSION: u32 = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientData {
|
||||
heads: HashMap<OutputId, (ZwlrOutputHeadV1, Vec<ZwlrOutputModeV1>)>,
|
||||
confs: HashMap<ZwlrOutputConfigurationV1, OutputConfigurationState>,
|
||||
manager: ZwlrOutputManagerV1,
|
||||
}
|
||||
|
||||
pub struct OutputManagementManagerState {
|
||||
display: DisplayHandle,
|
||||
serial: u32,
|
||||
clients: HashMap<ClientId, ClientData>,
|
||||
current_state: HashMap<OutputId, niri_ipc::Output>,
|
||||
current_config: niri_config::Outputs,
|
||||
}
|
||||
|
||||
pub struct OutputManagementManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
}
|
||||
|
||||
pub trait OutputManagementHandler {
|
||||
fn output_management_state(&mut self) -> &mut OutputManagementManagerState;
|
||||
fn apply_output_config(&mut self, config: niri_config::Outputs);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum OutputConfigurationState {
|
||||
Ongoing(HashMap<OutputId, niri_config::Output>),
|
||||
Finished,
|
||||
}
|
||||
|
||||
pub enum OutputConfigurationHeadState {
|
||||
Cancelled,
|
||||
Ok(OutputId, ZwlrOutputConfigurationV1),
|
||||
}
|
||||
|
||||
impl OutputManagementManagerState {
|
||||
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let global_data = OutputManagementManagerGlobalData {
|
||||
filter: Box::new(filter),
|
||||
};
|
||||
display.create_global::<D, ZwlrOutputManagerV1, _>(VERSION, global_data);
|
||||
|
||||
Self {
|
||||
display: display.clone(),
|
||||
clients: HashMap::new(),
|
||||
serial: 0,
|
||||
current_state: HashMap::new(),
|
||||
current_config: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_config_changed(&mut self, new_config: niri_config::Outputs) {
|
||||
self.current_config = new_config;
|
||||
}
|
||||
|
||||
pub fn notify_changes(&mut self, new_state: HashMap<OutputId, niri_ipc::Output>) {
|
||||
let mut changed = false; /* most likely to end up true */
|
||||
for (output, conf) in new_state.iter() {
|
||||
if let Some(old) = self.current_state.get(output) {
|
||||
if old.vrr_enabled != conf.vrr_enabled {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
if head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
|
||||
head.adaptive_sync(match conf.vrr_enabled {
|
||||
true => AdaptiveSyncState::Enabled,
|
||||
false => AdaptiveSyncState::Disabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TTY outputs can't change modes I think, however, winit and virtual outputs can.
|
||||
let modes_changed = old.modes != conf.modes;
|
||||
if modes_changed {
|
||||
changed = true;
|
||||
if old.modes.len() != conf.modes.len() {
|
||||
error!("output's old mode count doesn't match new modes");
|
||||
} else {
|
||||
for client in self.clients.values() {
|
||||
if let Some((_, modes)) = client.heads.get(output) {
|
||||
for (wl_mode, mode) in zip(modes, &conf.modes) {
|
||||
wl_mode.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
wl_mode.refresh(refresh_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (old.current_mode, conf.current_mode) {
|
||||
(Some(old_index), Some(new_index)) => {
|
||||
if old.modes.len() == conf.modes.len()
|
||||
&& (modes_changed || old_index != new_index)
|
||||
{
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, modes)) = client.heads.get(output) {
|
||||
if let Some(new_mode) = modes.get(new_index) {
|
||||
head.current_mode(new_mode);
|
||||
} else {
|
||||
error!(
|
||||
"output new mode doesnt exist for the client's output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
head.enabled(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(new_index)) => {
|
||||
if old.modes.len() == conf.modes.len() {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, modes)) = client.heads.get(output) {
|
||||
head.enabled(1);
|
||||
if let Some(mode) = modes.get(new_index) {
|
||||
head.current_mode(mode);
|
||||
} else {
|
||||
error!(
|
||||
"output new mode doesnt exist for the client's output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
match (old.logical, conf.logical) {
|
||||
(Some(old_logical), Some(new_logical)) => {
|
||||
if old_logical != new_logical {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
if old_logical.x != new_logical.x
|
||||
|| old_logical.y != new_logical.y
|
||||
{
|
||||
head.position(new_logical.x, new_logical.y);
|
||||
}
|
||||
if old_logical.scale != new_logical.scale {
|
||||
head.scale(new_logical.scale);
|
||||
}
|
||||
if old_logical.transform != new_logical.transform {
|
||||
head.transform(
|
||||
ipc_transform_to_smithay(new_logical.transform).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(new_logical)) => {
|
||||
changed = true;
|
||||
for client in self.clients.values() {
|
||||
if let Some((head, _)) = client.heads.get(output) {
|
||||
// head enable in the mode diff check
|
||||
head.position(new_logical.x, new_logical.y);
|
||||
head.transform(
|
||||
ipc_transform_to_smithay(new_logical.transform).into(),
|
||||
);
|
||||
head.scale(new_logical.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
// heads disabled in the mode diff check
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
} else {
|
||||
changed = true;
|
||||
notify_new_head(self, output, conf);
|
||||
}
|
||||
}
|
||||
for (old, _) in self.current_state.iter() {
|
||||
if !new_state.contains_key(old) {
|
||||
changed = true;
|
||||
notify_removed_head(&mut self.clients, old);
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.current_state = new_state;
|
||||
self.serial += 1;
|
||||
for data in self.clients.values() {
|
||||
data.manager.done(self.serial);
|
||||
for conf in data.confs.keys() {
|
||||
conf.cancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData, D>
|
||||
for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
state: &mut D,
|
||||
display: &DisplayHandle,
|
||||
client: &Client,
|
||||
manager: New<ZwlrOutputManagerV1>,
|
||||
_manager_state: &OutputManagementManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let manager = data_init.init(manager, ());
|
||||
let g_state = state.output_management_state();
|
||||
let mut client_data = ClientData {
|
||||
heads: HashMap::new(),
|
||||
confs: HashMap::new(),
|
||||
manager: manager.clone(),
|
||||
};
|
||||
for (output, conf) in &g_state.current_state {
|
||||
send_new_head::<D>(display, client, &mut client_data, *output, conf);
|
||||
}
|
||||
g_state.clients.insert(client.id(), client_data);
|
||||
manager.done(g_state.serial);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &OutputManagementManagerGlobalData) -> bool {
|
||||
(global_data.filter)(&client)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputManagerV1, (), D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
client: &Client,
|
||||
_manager: &ZwlrOutputManagerV1,
|
||||
request: zwlr_output_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => {
|
||||
let g_state = state.output_management_state();
|
||||
let conf = data_init.init(id, serial);
|
||||
if let Some(client_data) = g_state.clients.get_mut(&client.id()) {
|
||||
if serial != g_state.serial {
|
||||
conf.cancelled();
|
||||
}
|
||||
let state = OutputConfigurationState::Ongoing(HashMap::new());
|
||||
client_data.confs.insert(conf, state);
|
||||
} else {
|
||||
error!("CreateConfiguration: missing client data");
|
||||
}
|
||||
}
|
||||
zwlr_output_manager_v1::Request::Stop => {
|
||||
if let Some(c) = state.output_management_state().clients.remove(&client.id()) {
|
||||
c.manager.finished()
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputManagerV1, _data: &()) {
|
||||
state.output_management_state().clients.remove(&client);
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputConfigurationV1, u32, D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
client: &Client,
|
||||
conf: &ZwlrOutputConfigurationV1,
|
||||
request: zwlr_output_configuration_v1::Request,
|
||||
serial: &u32,
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let g_state = state.output_management_state();
|
||||
let outdated = *serial != g_state.serial;
|
||||
if outdated {
|
||||
debug!("OutputConfiguration: request from an outdated configuration");
|
||||
}
|
||||
|
||||
let new_config = g_state
|
||||
.clients
|
||||
.get_mut(&client.id())
|
||||
.and_then(|data| data.confs.get_mut(conf));
|
||||
if new_config.is_none() {
|
||||
error!("OutputConfiguration: request from unknown configuration object");
|
||||
}
|
||||
|
||||
match request {
|
||||
zwlr_output_configuration_v1::Request::EnableHead { id, head } => {
|
||||
let Some(output) = head.data::<OutputId>() else {
|
||||
error!("EnableHead: Missing attached output");
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
};
|
||||
if outdated {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) = new_config else {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output) else {
|
||||
error!("EnableHead: output missing from current config");
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
return;
|
||||
};
|
||||
|
||||
match new_config.entry(*output) {
|
||||
Entry::Occupied(_) => {
|
||||
let _fail = data_init.init(id, OutputConfigurationHeadState::Cancelled);
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
|
||||
"head has been already configured",
|
||||
);
|
||||
return;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let name = OutputName::from_ipc_output(current_config);
|
||||
let mut config = g_state
|
||||
.current_config
|
||||
.find(&name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| niri_config::Output {
|
||||
name: name.format_make_model_serial_or_connector(),
|
||||
..Default::default()
|
||||
});
|
||||
config.off = false;
|
||||
entry.insert(config);
|
||||
}
|
||||
};
|
||||
|
||||
data_init.init(id, OutputConfigurationHeadState::Ok(*output, conf.clone()));
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::DisableHead { head } => {
|
||||
if outdated {
|
||||
return;
|
||||
}
|
||||
let Some(output) = head.data::<OutputId>() else {
|
||||
error!("DisableHead: missing attached output head name");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) = new_config else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output) else {
|
||||
error!("EnableHead: output missing from current config");
|
||||
return;
|
||||
};
|
||||
|
||||
match new_config.entry(*output) {
|
||||
Entry::Occupied(_) => {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyConfiguredHead,
|
||||
"head has been already configured",
|
||||
);
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let name = OutputName::from_ipc_output(current_config);
|
||||
let mut config = g_state
|
||||
.current_config
|
||||
.find(&name)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| niri_config::Output {
|
||||
name: name.format_make_model_serial_or_connector(),
|
||||
..Default::default()
|
||||
});
|
||||
config.off = true;
|
||||
entry.insert(config);
|
||||
}
|
||||
};
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::Apply => {
|
||||
if outdated {
|
||||
conf.cancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) =
|
||||
mem::replace(new_config, OutputConfigurationState::Finished)
|
||||
else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let any_enabled = new_config.values().any(|c| !c.off);
|
||||
if !any_enabled {
|
||||
conf.failed();
|
||||
return;
|
||||
}
|
||||
|
||||
state.apply_output_config(new_config.into_values().collect());
|
||||
// FIXME: verify that it had been applied successfully (which may be difficult).
|
||||
conf.succeeded();
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::Test => {
|
||||
if outdated {
|
||||
conf.cancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(new_config) = new_config else {
|
||||
return;
|
||||
};
|
||||
|
||||
let OutputConfigurationState::Ongoing(new_config) =
|
||||
mem::replace(new_config, OutputConfigurationState::Finished)
|
||||
else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let any_enabled = new_config.values().any(|c| !c.off);
|
||||
if !any_enabled {
|
||||
conf.failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: actually test the configuration with TTY.
|
||||
conf.succeeded()
|
||||
}
|
||||
zwlr_output_configuration_v1::Request::Destroy => {
|
||||
g_state
|
||||
.clients
|
||||
.get_mut(&client.id())
|
||||
.map(|d| d.confs.remove(conf));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState, D>
|
||||
for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
state: &mut D,
|
||||
client: &Client,
|
||||
conf_head: &ZwlrOutputConfigurationHeadV1,
|
||||
request: zwlr_output_configuration_head_v1::Request,
|
||||
data: &OutputConfigurationHeadState,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
let g_state = state.output_management_state();
|
||||
let Some(client_data) = g_state.clients.get_mut(&client.id()) else {
|
||||
error!("ConfigurationHead: missing client data");
|
||||
return;
|
||||
};
|
||||
let OutputConfigurationHeadState::Ok(output_id, conf) = data else {
|
||||
warn!("ConfigurationHead: request sent to a cancelled head");
|
||||
return;
|
||||
};
|
||||
let Some(serial) = conf.data::<u32>() else {
|
||||
error!("ConfigurationHead: missing serial");
|
||||
return;
|
||||
};
|
||||
if *serial != g_state.serial {
|
||||
warn!("ConfigurationHead: request sent to an outdated");
|
||||
return;
|
||||
}
|
||||
let Some(new_config) = client_data.confs.get_mut(conf) else {
|
||||
error!("ConfigurationHead: unknown configuration");
|
||||
return;
|
||||
};
|
||||
let OutputConfigurationState::Ongoing(new_config) = new_config else {
|
||||
conf.post_error(
|
||||
zwlr_output_configuration_v1::Error::AlreadyUsed,
|
||||
"configuration had already been used",
|
||||
);
|
||||
return;
|
||||
};
|
||||
let Some(new_config) = new_config.get_mut(output_id) else {
|
||||
error!("ConfigurationHead: config missing from enabled heads");
|
||||
return;
|
||||
};
|
||||
|
||||
match request {
|
||||
zwlr_output_configuration_head_v1::Request::SetMode { mode } => {
|
||||
let index = match client_data
|
||||
.heads
|
||||
.get(output_id)
|
||||
.map(|(_, mods)| mods.iter().position(|m| m.id() == mode.id()))
|
||||
{
|
||||
Some(Some(index)) => index,
|
||||
_ => {
|
||||
warn!("SetMode: failed to find requested mode");
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidMode,
|
||||
"failed to find requested mode",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output_id) else {
|
||||
warn!("SetMode: output missing from the current config");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(mode) = current_config.modes.get(index) else {
|
||||
error!("SetMode: requested mode is out of range");
|
||||
return;
|
||||
};
|
||||
|
||||
new_config.mode = Some(niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
});
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetCustomMode {
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
} => {
|
||||
// FIXME: Support custom mode
|
||||
let (width, height, refresh): (u16, u16, u32) =
|
||||
match (width.try_into(), height.try_into(), refresh.try_into()) {
|
||||
(Ok(width), Ok(height), Ok(refresh)) => (width, height, refresh),
|
||||
_ => {
|
||||
warn!("SetCustomMode: invalid input data");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output_id) else {
|
||||
warn!("SetMode: output missing from the current config");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(mode) = current_config.modes.iter().find(|m| {
|
||||
m.width == width
|
||||
&& m.height == height
|
||||
&& (refresh == 0 || m.refresh_rate == refresh)
|
||||
}) else {
|
||||
warn!("SetCustomMode: no matching mode");
|
||||
return;
|
||||
};
|
||||
|
||||
new_config.mode = Some(niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
});
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => {
|
||||
new_config.position = Some(niri_config::Position { x, y });
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetTransform { transform } => {
|
||||
let transform = match transform {
|
||||
WEnum::Value(WlTransform::Normal) => Transform::Normal,
|
||||
WEnum::Value(WlTransform::_90) => Transform::_90,
|
||||
WEnum::Value(WlTransform::_180) => Transform::_180,
|
||||
WEnum::Value(WlTransform::_270) => Transform::_270,
|
||||
WEnum::Value(WlTransform::Flipped) => Transform::Flipped,
|
||||
WEnum::Value(WlTransform::Flipped90) => Transform::Flipped90,
|
||||
WEnum::Value(WlTransform::Flipped180) => Transform::Flipped180,
|
||||
WEnum::Value(WlTransform::Flipped270) => Transform::Flipped270,
|
||||
_ => {
|
||||
warn!("SetTransform: unknown requested transform");
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidTransform,
|
||||
"unknown transform value",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
new_config.transform = transform;
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetScale { scale } => {
|
||||
if scale <= 0. {
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidScale,
|
||||
"scale is negative or zero",
|
||||
);
|
||||
return;
|
||||
}
|
||||
new_config.scale = Some(FloatOrInt(scale));
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => {
|
||||
let vrr = match state {
|
||||
WEnum::Value(AdaptiveSyncState::Enabled) => Some(Vrr { on_demand: false }),
|
||||
WEnum::Value(AdaptiveSyncState::Disabled) => None,
|
||||
_ => {
|
||||
warn!("SetAdaptativeSync: unknown requested adaptative sync");
|
||||
conf_head.post_error(
|
||||
zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState,
|
||||
"unknown adaptive sync value",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
new_config.variable_refresh_rate = vrr;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputHeadV1, OutputId, D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_output_head: &ZwlrOutputHeadV1,
|
||||
request: zwlr_output_head_v1::Request,
|
||||
_data: &OutputId,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_output_head_v1::Request::Release => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
fn destroyed(state: &mut D, client: ClientId, _resource: &ZwlrOutputHeadV1, data: &OutputId) {
|
||||
if let Some(c) = state.output_management_state().clients.get_mut(&client) {
|
||||
c.heads.remove(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Dispatch<ZwlrOutputModeV1, (), D> for OutputManagementManagerState
|
||||
where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
{
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_mode: &ZwlrOutputModeV1,
|
||||
request: zwlr_output_mode_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_output_mode_v1::Request::Release => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delegate_output_management{
|
||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::protocols::output_management::OutputManagementManagerGlobalData
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: ()
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: u32
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: $crate::backend::OutputId
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: ()
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||
smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: $crate::protocols::output_management::OutputConfigurationHeadState
|
||||
] => $crate::protocols::output_management::OutputManagementManagerState);
|
||||
};
|
||||
}
|
||||
|
||||
fn notify_removed_head(clients: &mut HashMap<ClientId, ClientData>, head: &OutputId) {
|
||||
for data in clients.values_mut() {
|
||||
if let Some((head, mods)) = data.heads.remove(head) {
|
||||
mods.iter().for_each(|m| m.finished());
|
||||
head.finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_new_head(
|
||||
state: &mut OutputManagementManagerState,
|
||||
output: &OutputId,
|
||||
conf: &niri_ipc::Output,
|
||||
) {
|
||||
let display = &state.display;
|
||||
let clients = &mut state.clients;
|
||||
for data in clients.values_mut() {
|
||||
if let Some(client) = data.manager.client() {
|
||||
send_new_head::<State>(display, &client, data, *output, conf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_new_head<D>(
|
||||
display: &DisplayHandle,
|
||||
client: &Client,
|
||||
client_data: &mut ClientData,
|
||||
output: OutputId,
|
||||
conf: &niri_ipc::Output,
|
||||
) where
|
||||
D: GlobalDispatch<ZwlrOutputManagerV1, OutputManagementManagerGlobalData>,
|
||||
D: Dispatch<ZwlrOutputManagerV1, ()>,
|
||||
D: Dispatch<ZwlrOutputConfigurationV1, u32>,
|
||||
D: Dispatch<ZwlrOutputConfigurationHeadV1, OutputConfigurationHeadState>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: OutputManagementHandler,
|
||||
D: 'static,
|
||||
D: Dispatch<ZwlrOutputHeadV1, OutputId>,
|
||||
D: Dispatch<ZwlrOutputModeV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let new_head = client
|
||||
.create_resource::<ZwlrOutputHeadV1, _, D>(display, client_data.manager.version(), output)
|
||||
.unwrap();
|
||||
client_data.manager.head(&new_head);
|
||||
new_head.name(conf.name.clone());
|
||||
// Format matches what Output::new() does internally.
|
||||
new_head.description(format!("{} - {} - {}", conf.make, conf.model, conf.name));
|
||||
if let Some((width, height)) = conf.physical_size {
|
||||
if let (Ok(a), Ok(b)) = (width.try_into(), height.try_into()) {
|
||||
new_head.physical_size(a, b);
|
||||
}
|
||||
}
|
||||
let mut new_modes = Vec::with_capacity(conf.modes.len());
|
||||
for (index, mode) in conf.modes.iter().enumerate() {
|
||||
let new_mode = client
|
||||
.create_resource::<ZwlrOutputModeV1, _, D>(display, new_head.version(), ())
|
||||
.unwrap();
|
||||
new_head.mode(&new_mode);
|
||||
new_mode.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if mode.is_preferred {
|
||||
new_mode.preferred();
|
||||
}
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
new_mode.refresh(refresh_rate);
|
||||
}
|
||||
if Some(index) == conf.current_mode {
|
||||
new_head.current_mode(&new_mode);
|
||||
}
|
||||
new_modes.push(new_mode);
|
||||
}
|
||||
if let Some(logical) = conf.logical {
|
||||
new_head.position(logical.x, logical.y);
|
||||
new_head.transform(ipc_transform_to_smithay(logical.transform).into());
|
||||
new_head.scale(logical.scale);
|
||||
}
|
||||
new_head.enabled(conf.current_mode.is_some() as i32);
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE {
|
||||
new_head.make(conf.make.clone());
|
||||
}
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_MODEL_SINCE {
|
||||
new_head.model(conf.model.clone());
|
||||
}
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_SERIAL_NUMBER_SINCE {
|
||||
if let Some(serial) = &conf.serial {
|
||||
new_head.serial_number(serial.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if new_head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
|
||||
new_head.adaptive_sync(match conf.vrr_enabled {
|
||||
true => AdaptiveSyncState::Enabled,
|
||||
false => AdaptiveSyncState::Disabled,
|
||||
});
|
||||
}
|
||||
// new_head.serial_number(output.serial);
|
||||
client_data.heads.insert(output, (new_head, new_modes));
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
pub mod mutter_x11_interop {
|
||||
pub mod v1 {
|
||||
pub use self::generated::server;
|
||||
|
||||
mod generated {
|
||||
pub mod server {
|
||||
#![allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
|
||||
#![allow(non_upper_case_globals, non_snake_case, unused_imports)]
|
||||
#![allow(missing_docs, clippy::all)]
|
||||
|
||||
use smithay::reexports::wayland_server;
|
||||
use wayland_server::protocol::*;
|
||||
|
||||
pub mod __interfaces {
|
||||
use smithay::reexports::wayland_server;
|
||||
use wayland_server::protocol::__interfaces::*;
|
||||
wayland_scanner::generate_interfaces!("resources/mutter-x11-interop.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_server_code!("resources/mutter-x11-interop.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+189
-79
@@ -1,7 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use std::time::Duration;
|
||||
|
||||
use calloop::generic::Generic;
|
||||
use calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||
Flags, ZwlrScreencopyFrameV1,
|
||||
@@ -11,17 +18,62 @@ 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::protocol::wl_shm::Format;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size};
|
||||
use smithay::wayland::shm;
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
|
||||
use smithay::wayland::{dmabuf, shm};
|
||||
|
||||
// We do not support copy_with_damage() semantics yet.
|
||||
const VERSION: u32 = 1;
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
pub struct ScreencopyManagerState;
|
||||
const VERSION: u32 = 3;
|
||||
|
||||
pub struct ScreencopyQueue {
|
||||
damage_tracker: OutputDamageTracker,
|
||||
screencopies: Vec<Screencopy>,
|
||||
}
|
||||
|
||||
impl Default for ScreencopyQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreencopyQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
damage_tracker: OutputDamageTracker::new((0, 0), 1.0, Transform::Normal),
|
||||
screencopies: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(&mut self) -> (&mut OutputDamageTracker, Option<&Screencopy>) {
|
||||
let ScreencopyQueue {
|
||||
damage_tracker,
|
||||
screencopies,
|
||||
} = self;
|
||||
(damage_tracker, screencopies.first())
|
||||
}
|
||||
|
||||
pub fn push(&mut self, screencopy: Screencopy) {
|
||||
self.screencopies.push(screencopy);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Screencopy {
|
||||
self.screencopies.pop().unwrap()
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, output: &Output) {
|
||||
self.screencopies
|
||||
.retain(|screencopy| screencopy.output() != output);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScreencopyManagerState {
|
||||
queues: HashMap<ZwlrScreencopyManagerV1, ScreencopyQueue>,
|
||||
}
|
||||
|
||||
pub struct ScreencopyManagerGlobalData {
|
||||
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
|
||||
@@ -42,7 +94,28 @@ impl ScreencopyManagerState {
|
||||
};
|
||||
display.create_global::<D, ZwlrScreencopyManagerV1, _>(VERSION, global_data);
|
||||
|
||||
Self
|
||||
Self {
|
||||
queues: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, manager: &ZwlrScreencopyManagerV1) {
|
||||
// Clean up all entries if its manager is dead and its queue is empty.
|
||||
self.queues
|
||||
.retain(|k, v| k.is_alive() || !v.screencopies.is_empty());
|
||||
|
||||
self.queues.insert(manager.clone(), ScreencopyQueue::new());
|
||||
}
|
||||
|
||||
pub fn get_queue_mut(
|
||||
&mut self,
|
||||
manager: &ZwlrScreencopyManagerV1,
|
||||
) -> Option<&mut ScreencopyQueue> {
|
||||
self.queues.get_mut(manager)
|
||||
}
|
||||
|
||||
pub fn queues_mut(&mut self) -> impl Iterator<Item = &mut ScreencopyQueue> {
|
||||
self.queues.values_mut()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +129,15 @@ where
|
||||
D: 'static,
|
||||
{
|
||||
fn bind(
|
||||
_state: &mut D,
|
||||
state: &mut D,
|
||||
_display: &DisplayHandle,
|
||||
_client: &Client,
|
||||
manager: New<ZwlrScreencopyManagerV1>,
|
||||
_manager_state: &ScreencopyManagerGlobalData,
|
||||
data_init: &mut DataInit<'_, D>,
|
||||
) {
|
||||
data_init.init(manager, ());
|
||||
let manager = data_init.init(manager, ());
|
||||
state.screencopy_state().bind(&manager);
|
||||
}
|
||||
|
||||
fn can_view(client: Client, global_data: &ScreencopyManagerGlobalData) -> bool {
|
||||
@@ -82,7 +156,7 @@ where
|
||||
fn request(
|
||||
_state: &mut D,
|
||||
_client: &Client,
|
||||
_manager: &ZwlrScreencopyManagerV1,
|
||||
manager: &ZwlrScreencopyManagerV1,
|
||||
request: zwlr_screencopy_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
@@ -136,8 +210,8 @@ where
|
||||
|
||||
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);
|
||||
let output_scale = output.current_scale().fractional_scale();
|
||||
let physical_rect = rect.to_physical_precise_round(output_scale);
|
||||
|
||||
// Clamp captured region to the output.
|
||||
let Some(clamped_rect) = physical_rect.intersection(output_rect) else {
|
||||
@@ -174,6 +248,7 @@ where
|
||||
let frame = data_init.init(
|
||||
frame,
|
||||
ScreencopyFrameState::Pending {
|
||||
manager: manager.clone(),
|
||||
info,
|
||||
copied: Arc::new(AtomicBool::new(false)),
|
||||
},
|
||||
@@ -181,30 +256,31 @@ where
|
||||
|
||||
// Send desired SHM buffer parameters.
|
||||
frame.buffer(
|
||||
wl_shm::Format::Argb8888,
|
||||
Format::Xrgb8888,
|
||||
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();
|
||||
// }
|
||||
if frame.version() >= 3 {
|
||||
// Send desired DMA buffer parameters.
|
||||
frame.linux_dmabuf(
|
||||
Fourcc::Xrgb8888 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);
|
||||
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy);
|
||||
fn screencopy_state(&mut self) -> &mut ScreencopyManagerState;
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
@@ -236,6 +312,7 @@ pub struct ScreencopyFrameInfo {
|
||||
pub enum ScreencopyFrameState {
|
||||
Failed,
|
||||
Pending {
|
||||
manager: ZwlrScreencopyManagerV1,
|
||||
info: ScreencopyFrameInfo,
|
||||
copied: Arc<AtomicBool>,
|
||||
},
|
||||
@@ -260,9 +337,13 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
let (info, copied) = match data {
|
||||
ScreencopyFrameState::Failed => return,
|
||||
ScreencopyFrameState::Pending { info, copied } => (info, copied),
|
||||
let ScreencopyFrameState::Pending {
|
||||
manager,
|
||||
info,
|
||||
copied,
|
||||
} = data
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if copied.load(Ordering::SeqCst) {
|
||||
@@ -275,44 +356,71 @@ where
|
||||
|
||||
let (buffer, with_damage) = match request {
|
||||
zwlr_screencopy_frame_v1::Request::Copy { buffer } => (buffer, false),
|
||||
// zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => (buffer, true),
|
||||
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
|
||||
let size = info.buffer_size;
|
||||
|
||||
let buffer = if let Ok(dmabuf) = dmabuf::get_dmabuf(&buffer) {
|
||||
if dmabuf.format().code == Fourcc::Xrgb8888
|
||||
&& dmabuf.width() == size.w as u32
|
||||
&& dmabuf.height() == size.h as u32
|
||||
{
|
||||
ScreencopyBuffer::Dmabuf(dmabuf.clone())
|
||||
} else {
|
||||
frame.post_error(
|
||||
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
|
||||
"invalid dmabuf parameters",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if shm::with_buffer_contents(&buffer, |_, shm_len, buffer_data| {
|
||||
buffer_data.format == Format::Xrgb8888
|
||||
&& buffer_data.width == size.w
|
||||
&& buffer_data.height == size.h
|
||||
&& buffer_data.stride == size.w * 4
|
||||
&& shm_len == buffer_data.stride as usize * buffer_data.height as usize
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
ScreencopyBuffer::Shm(buffer)
|
||||
} else {
|
||||
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,
|
||||
});
|
||||
state.frame(
|
||||
manager,
|
||||
Screencopy {
|
||||
buffer,
|
||||
frame: frame.clone(),
|
||||
info: info.clone(),
|
||||
with_damage,
|
||||
submitted: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Screencopy buffer.
|
||||
#[derive(Clone)]
|
||||
pub enum ScreencopyBuffer {
|
||||
Dmabuf(Dmabuf),
|
||||
Shm(WlBuffer),
|
||||
}
|
||||
|
||||
/// Screencopy frame.
|
||||
pub struct Screencopy {
|
||||
info: ScreencopyFrameInfo,
|
||||
frame: ZwlrScreencopyFrameV1,
|
||||
#[allow(unused)]
|
||||
buffer: ScreencopyBuffer,
|
||||
with_damage: bool,
|
||||
buffer: WlBuffer,
|
||||
submitted: bool,
|
||||
}
|
||||
|
||||
@@ -326,7 +434,7 @@ impl Drop for Screencopy {
|
||||
|
||||
impl Screencopy {
|
||||
/// Get the target buffer to copy to.
|
||||
pub fn buffer(&self) -> &WlBuffer {
|
||||
pub fn buffer(&self) -> &ScreencopyBuffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
@@ -346,17 +454,19 @@ impl Screencopy {
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
pub fn with_damage(&self) -> bool {
|
||||
self.with_damage
|
||||
}
|
||||
|
||||
pub fn damage(&self, damages: impl Iterator<Item = Rectangle<i32, smithay::utils::Buffer>>) {
|
||||
for Rectangle { loc, size } in damages {
|
||||
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) {
|
||||
fn submit(mut self, y_invert: bool, timestamp: Duration) {
|
||||
// Notify client that buffer is ordinary.
|
||||
self.frame.flags(if y_invert {
|
||||
Flags::YInvert
|
||||
@@ -365,34 +475,34 @@ impl Screencopy {
|
||||
});
|
||||
|
||||
// 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();
|
||||
let tv_sec_hi = (timestamp.as_secs() >> 32) as u32;
|
||||
let tv_sec_lo = (timestamp.as_secs() & 0xFFFFFFFF) as u32;
|
||||
let tv_nsec = timestamp.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();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
pub fn submit_after_sync<T>(
|
||||
self,
|
||||
y_invert: bool,
|
||||
sync_point: Option<SyncPoint>,
|
||||
event_loop: &LoopHandle<'_, T>,
|
||||
) {
|
||||
let timestamp = get_monotonic_time();
|
||||
match sync_point.and_then(|s| s.export()) {
|
||||
None => self.submit(y_invert, timestamp),
|
||||
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, timestamp);
|
||||
Ok(PostAction::Remove)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+518
-146
@@ -1,12 +1,15 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use calloop::RegistrationToken;
|
||||
use pipewire::context::Context;
|
||||
use pipewire::core::Core;
|
||||
use pipewire::main_loop::MainLoop;
|
||||
@@ -16,20 +19,24 @@ use pipewire::spa::param::format::{FormatProperties, MediaSubtype, MediaType};
|
||||
use pipewire::spa::param::format_utils::parse_format;
|
||||
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
|
||||
use pipewire::spa::param::ParamType;
|
||||
use pipewire::spa::pod::deserialize::PodDeserializer;
|
||||
use pipewire::spa::pod::serialize::PodSerializer;
|
||||
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
|
||||
use pipewire::spa::pod::{self, ChoiceValue, Pod, PodPropFlags, Property, PropertyFlags};
|
||||
use pipewire::spa::sys::*;
|
||||
use pipewire::spa::utils::{
|
||||
Choice, ChoiceEnum, ChoiceFlags, Direction, Fraction, Rectangle, SpaTypes,
|
||||
};
|
||||
use pipewire::spa::{self};
|
||||
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
|
||||
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
||||
use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::allocator::{Format, Fourcc};
|
||||
use smithay::backend::drm::DrmDeviceFd;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::WeakOutput;
|
||||
use smithay::output::{Output, OutputModeSource, WeakOutput};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
@@ -39,6 +46,10 @@ use zbus::SignalContext;
|
||||
use crate::dbus::mutter_screen_cast::{self, CursorMode};
|
||||
use crate::niri::State;
|
||||
use crate::render_helpers::render_to_dmabuf;
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
// Give a 0.1 ms allowance for presentation time errors.
|
||||
const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
|
||||
|
||||
pub struct PipeWire {
|
||||
_context: Context,
|
||||
@@ -57,22 +68,35 @@ pub struct Cast {
|
||||
_listener: StreamListener<()>,
|
||||
pub is_active: Rc<Cell<bool>>,
|
||||
pub target: CastTarget,
|
||||
pub size: Rc<Cell<CastSize>>,
|
||||
pub refresh: u32,
|
||||
formats: FormatSet,
|
||||
state: Rc<RefCell<CastState>>,
|
||||
refresh: Rc<Cell<u32>>,
|
||||
offer_alpha: bool,
|
||||
pub cursor_mode: CursorMode,
|
||||
pub last_frame_time: Duration,
|
||||
pub min_time_between_frames: Rc<Cell<Duration>>,
|
||||
pub dmabufs: Rc<RefCell<HashMap<i32, Dmabuf>>>,
|
||||
min_time_between_frames: Rc<Cell<Duration>>,
|
||||
dmabufs: Rc<RefCell<HashMap<i64, Dmabuf>>>,
|
||||
scheduled_redraw: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CastSize {
|
||||
InitialPending(Size<i32, Physical>),
|
||||
Ready(Size<i32, Physical>),
|
||||
ChangePending {
|
||||
last_negotiated: Size<i32, Physical>,
|
||||
pending: Size<i32, Physical>,
|
||||
#[derive(Debug)]
|
||||
pub enum CastState {
|
||||
ResizePending {
|
||||
pending_size: Size<u32, Physical>,
|
||||
},
|
||||
ConfirmationPending {
|
||||
size: Size<u32, Physical>,
|
||||
alpha: bool,
|
||||
modifier: Modifier,
|
||||
plane_count: i32,
|
||||
},
|
||||
Ready {
|
||||
size: Size<u32, Physical>,
|
||||
alpha: bool,
|
||||
modifier: Modifier,
|
||||
plane_count: i32,
|
||||
// Lazily-initialized to keep the initialization to a single place.
|
||||
damage_tracker: Option<OutputDamageTracker>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -89,17 +113,17 @@ pub enum CastTarget {
|
||||
}
|
||||
|
||||
macro_rules! make_params {
|
||||
($params:ident, $size:expr, $refresh:expr, $alpha:expr) => {
|
||||
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
|
||||
let mut b1 = Vec::new();
|
||||
let mut b2 = Vec::new();
|
||||
|
||||
let o1 = make_video_params($size, $refresh, false);
|
||||
let o1 = make_video_params($formats, $size, $refresh, false);
|
||||
let pod1 = make_pod(&mut b1, o1);
|
||||
|
||||
let mut p1;
|
||||
let mut p2;
|
||||
$params = if $alpha {
|
||||
let o2 = make_video_params($size, $refresh, true);
|
||||
let o2 = make_video_params($formats, $size, $refresh, true);
|
||||
p2 = [pod1, make_pod(&mut b2, o2)];
|
||||
&mut p2[..]
|
||||
} else {
|
||||
@@ -157,6 +181,7 @@ impl PipeWire {
|
||||
pub fn start_cast(
|
||||
&self,
|
||||
gbm: GbmDevice<DrmDeviceFd>,
|
||||
formats: FormatSet,
|
||||
session_id: usize,
|
||||
target: CastTarget,
|
||||
size: Size<i32, Physical>,
|
||||
@@ -190,10 +215,10 @@ impl PipeWire {
|
||||
let is_active = Rc::new(Cell::new(false));
|
||||
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
|
||||
let dmabufs = Rc::new(RefCell::new(HashMap::new()));
|
||||
let negotiated_alpha = Rc::new(Cell::new(false));
|
||||
let refresh = Rc::new(Cell::new(refresh));
|
||||
|
||||
let pending_size = size;
|
||||
let size = Rc::new(Cell::new(CastSize::InitialPending(size)));
|
||||
let pending_size = Size::from((size.w as u32, size.h as u32));
|
||||
let state = Rc::new(RefCell::new(CastState::ResizePending { pending_size }));
|
||||
|
||||
let listener = stream
|
||||
.add_local_listener_with_user_data(())
|
||||
@@ -244,8 +269,11 @@ impl PipeWire {
|
||||
})
|
||||
.param_changed({
|
||||
let min_time_between_frames = min_time_between_frames.clone();
|
||||
let size = size.clone();
|
||||
let negotiated_alpha = negotiated_alpha.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
let state = state.clone();
|
||||
let gbm = gbm.clone();
|
||||
let formats = formats.clone();
|
||||
let refresh = refresh.clone();
|
||||
move |stream, (), id, pod| {
|
||||
let id = ParamType::from_raw(id);
|
||||
trace!(?id, "pw stream: param_changed");
|
||||
@@ -270,33 +298,200 @@ impl PipeWire {
|
||||
|
||||
let mut format = VideoInfoRaw::new();
|
||||
format.parse(pod).unwrap();
|
||||
trace!("pw stream: got format = {format:?}");
|
||||
debug!("pw stream: got format = {format:?}");
|
||||
|
||||
let expected_size = size.get().expected_format_size();
|
||||
let format_size =
|
||||
Size::from((format.size().width as i32, format.size().height as i32));
|
||||
let format_size = Size::from((format.size().width, format.size().height));
|
||||
|
||||
if format_size == expected_size {
|
||||
size.set(CastSize::Ready(expected_size));
|
||||
} else {
|
||||
size.set(CastSize::ChangePending {
|
||||
last_negotiated: format_size,
|
||||
pending: expected_size,
|
||||
});
|
||||
let mut state = state.borrow_mut();
|
||||
if format_size != state.expected_format_size() {
|
||||
if !matches!(&*state, CastState::ResizePending { .. }) {
|
||||
warn!("pw stream: wrong size, but we're not resizing");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
|
||||
debug!("pw stream: wrong size, waiting");
|
||||
return;
|
||||
}
|
||||
|
||||
negotiated_alpha.set(format.format() == VideoFormat::BGRA);
|
||||
let format_has_alpha = format.format() == VideoFormat::BGRA;
|
||||
let fourcc = if format_has_alpha {
|
||||
Fourcc::Argb8888
|
||||
} else {
|
||||
Fourcc::Xrgb8888
|
||||
};
|
||||
|
||||
let max_frame_rate = format.max_framerate();
|
||||
// Subtract 0.5 ms to improve edge cases when equal to refresh rate.
|
||||
let min_frame_time = Duration::from_secs_f64(
|
||||
max_frame_rate.denom as f64 / max_frame_rate.num as f64,
|
||||
) - Duration::from_micros(500);
|
||||
let min_frame_time = Duration::from_micros(
|
||||
1_000_000 * u64::from(max_frame_rate.denom) / u64::from(max_frame_rate.num),
|
||||
);
|
||||
min_time_between_frames.set(min_frame_time);
|
||||
|
||||
const BPP: u32 = 4;
|
||||
let stride = format.size().width * BPP;
|
||||
let size = stride * format.size().height;
|
||||
let object = pod.as_object().unwrap();
|
||||
let Some(prop_modifier) =
|
||||
object.find_prop(spa::utils::Id(FormatProperties::VideoModifier.0))
|
||||
else {
|
||||
warn!("pw stream: modifier prop missing");
|
||||
stop_cast();
|
||||
return;
|
||||
};
|
||||
|
||||
if prop_modifier.flags().contains(PodPropFlags::DONT_FIXATE) {
|
||||
debug!("pw stream: fixating the modifier");
|
||||
|
||||
let pod_modifier = prop_modifier.value();
|
||||
let Ok((_, modifiers)) = PodDeserializer::deserialize_from::<Choice<i64>>(
|
||||
pod_modifier.as_bytes(),
|
||||
) else {
|
||||
warn!("pw stream: wrong modifier property type");
|
||||
stop_cast();
|
||||
return;
|
||||
};
|
||||
|
||||
let ChoiceEnum::Enum { alternatives, .. } = modifiers.1 else {
|
||||
warn!("pw stream: wrong modifier choice type");
|
||||
stop_cast();
|
||||
return;
|
||||
};
|
||||
|
||||
let (modifier, plane_count) = match find_preferred_modifier(
|
||||
&gbm,
|
||||
format_size,
|
||||
fourcc,
|
||||
alternatives,
|
||||
) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!("pw stream: couldn't find preferred modifier: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"pw stream: allocation successful \
|
||||
(modifier={modifier:?}, plane_count={plane_count}), \
|
||||
moving to confirmation pending"
|
||||
);
|
||||
|
||||
*state = CastState::ConfirmationPending {
|
||||
size: format_size,
|
||||
alpha: format_has_alpha,
|
||||
modifier,
|
||||
plane_count: plane_count as i32,
|
||||
};
|
||||
|
||||
let fixated_format = FormatSet::from_iter([Format {
|
||||
code: fourcc,
|
||||
modifier,
|
||||
}]);
|
||||
|
||||
let mut b1 = Vec::new();
|
||||
let mut b2 = Vec::new();
|
||||
|
||||
let o1 = make_video_params(
|
||||
&fixated_format,
|
||||
format_size,
|
||||
refresh.get(),
|
||||
format_has_alpha,
|
||||
);
|
||||
let pod1 = make_pod(&mut b1, o1);
|
||||
|
||||
let o2 = make_video_params(
|
||||
&formats,
|
||||
format_size,
|
||||
refresh.get(),
|
||||
format_has_alpha,
|
||||
);
|
||||
let mut params = [pod1, make_pod(&mut b2, o2)];
|
||||
|
||||
if let Err(err) = stream.update_params(&mut params) {
|
||||
warn!("error updating stream params: {err:?}");
|
||||
stop_cast();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify that alpha and modifier didn't change.
|
||||
let plane_count = match &*state {
|
||||
CastState::ConfirmationPending {
|
||||
size,
|
||||
alpha,
|
||||
modifier,
|
||||
plane_count,
|
||||
}
|
||||
| CastState::Ready {
|
||||
size,
|
||||
alpha,
|
||||
modifier,
|
||||
plane_count,
|
||||
..
|
||||
} if *alpha == format_has_alpha
|
||||
&& *modifier == Modifier::from(format.modifier()) =>
|
||||
{
|
||||
let size = *size;
|
||||
let alpha = *alpha;
|
||||
let modifier = *modifier;
|
||||
let plane_count = *plane_count;
|
||||
|
||||
let damage_tracker =
|
||||
if let CastState::Ready { damage_tracker, .. } = &mut *state {
|
||||
damage_tracker.take()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
debug!("pw stream: moving to ready state");
|
||||
|
||||
*state = CastState::Ready {
|
||||
size,
|
||||
alpha,
|
||||
modifier,
|
||||
plane_count,
|
||||
damage_tracker,
|
||||
};
|
||||
|
||||
plane_count
|
||||
}
|
||||
_ => {
|
||||
// We're negotiating a single modifier, or alpha or modifier changed,
|
||||
// so we need to do a test allocation.
|
||||
let (modifier, plane_count) = match find_preferred_modifier(
|
||||
&gbm,
|
||||
format_size,
|
||||
fourcc,
|
||||
vec![format.modifier() as i64],
|
||||
) {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
warn!("pw stream: test allocation failed: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"pw stream: allocation successful \
|
||||
(modifier={modifier:?}, plane_count={plane_count}), \
|
||||
moving to ready"
|
||||
);
|
||||
|
||||
*state = CastState::Ready {
|
||||
size: format_size,
|
||||
alpha: format_has_alpha,
|
||||
modifier,
|
||||
plane_count: plane_count as i32,
|
||||
damage_tracker: None,
|
||||
};
|
||||
|
||||
plane_count as i32
|
||||
}
|
||||
};
|
||||
|
||||
// const BPP: u32 = 4;
|
||||
// let stride = format.size().width * BPP;
|
||||
// let size = stride * format.size().height;
|
||||
|
||||
let o1 = pod::object!(
|
||||
SpaTypes::ObjectParamBuffers,
|
||||
@@ -312,10 +507,10 @@ impl PipeWire {
|
||||
}
|
||||
))),
|
||||
),
|
||||
Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(1)),
|
||||
Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)),
|
||||
Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)),
|
||||
Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)),
|
||||
Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(plane_count)),
|
||||
// Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)),
|
||||
// Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)),
|
||||
// Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)),
|
||||
Property::new(
|
||||
SPA_PARAM_BUFFERS_dataType,
|
||||
pod::Value::Choice(ChoiceValue::Int(Choice(
|
||||
@@ -345,25 +540,38 @@ impl PipeWire {
|
||||
let mut params = [
|
||||
make_pod(&mut b1, o1), // make_pod(&mut b2, o2)
|
||||
];
|
||||
stream.update_params(&mut params).unwrap();
|
||||
|
||||
if let Err(err) = stream.update_params(&mut params) {
|
||||
warn!("error updating stream params: {err:?}");
|
||||
stop_cast();
|
||||
}
|
||||
}
|
||||
})
|
||||
.add_buffer({
|
||||
let dmabufs = dmabufs.clone();
|
||||
let stop_cast = stop_cast.clone();
|
||||
let size = size.clone();
|
||||
let negotiated_alpha = negotiated_alpha.clone();
|
||||
let state = state.clone();
|
||||
move |stream, (), buffer| {
|
||||
let size = size.get().negotiated_size();
|
||||
let alpha = negotiated_alpha.get();
|
||||
trace!("pw stream: add_buffer, size={:?}, alpha={alpha}", size);
|
||||
let size = size.expect("size must be negotiated to allocate buffers");
|
||||
let (size, alpha, modifier) = if let CastState::Ready {
|
||||
size,
|
||||
alpha,
|
||||
modifier,
|
||||
..
|
||||
} = &*state.borrow()
|
||||
{
|
||||
(*size, *alpha, *modifier)
|
||||
} else {
|
||||
trace!("pw stream: add buffer, but not ready yet");
|
||||
return;
|
||||
};
|
||||
|
||||
trace!(
|
||||
"pw stream: add_buffer, size={size:?}, alpha={alpha}, \
|
||||
modifier={modifier:?}"
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let spa_buffer = (*buffer).buffer;
|
||||
let spa_data = (*spa_buffer).datas;
|
||||
assert!((*spa_buffer).n_datas > 0);
|
||||
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
|
||||
|
||||
let fourcc = if alpha {
|
||||
Fourcc::Argb8888
|
||||
@@ -371,36 +579,29 @@ impl PipeWire {
|
||||
Fourcc::Xrgb8888
|
||||
};
|
||||
|
||||
let bo = match gbm.create_buffer_object::<()>(
|
||||
size.w as u32,
|
||||
size.h as u32,
|
||||
fourcc,
|
||||
GbmBufferFlags::RENDERING | GbmBufferFlags::LINEAR,
|
||||
) {
|
||||
Ok(bo) => bo,
|
||||
Err(err) => {
|
||||
warn!("error creating GBM buffer object: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let buffer = GbmBuffer::from_bo(bo, true);
|
||||
let dmabuf = match buffer.export() {
|
||||
let dmabuf = match allocate_dmabuf(&gbm, size, fourcc, modifier) {
|
||||
Ok(dmabuf) => dmabuf,
|
||||
Err(err) => {
|
||||
warn!("error exporting GBM buffer object as dmabuf: {err:?}");
|
||||
warn!("error allocating dmabuf: {err:?}");
|
||||
stop_cast();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let fd = dmabuf.handles().next().unwrap().as_raw_fd();
|
||||
let plane_count = dmabuf.num_planes();
|
||||
assert_eq!((*spa_buffer).n_datas as usize, plane_count);
|
||||
|
||||
(*spa_data).type_ = DataType::DmaBuf.as_raw();
|
||||
(*spa_data).maxsize = dmabuf.strides().next().unwrap() * size.h as u32;
|
||||
(*spa_data).fd = fd as i64;
|
||||
(*spa_data).flags = SPA_DATA_FLAG_READWRITE;
|
||||
for (i, fd) in dmabuf.handles().enumerate() {
|
||||
let spa_data = (*spa_buffer).datas.add(i);
|
||||
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
|
||||
|
||||
(*spa_data).type_ = DataType::DmaBuf.as_raw();
|
||||
(*spa_data).maxsize = 1;
|
||||
(*spa_data).fd = fd.as_raw_fd() as i64;
|
||||
(*spa_data).flags = SPA_DATA_FLAG_READWRITE;
|
||||
}
|
||||
|
||||
let fd = (*(*spa_buffer).datas).fd;
|
||||
assert!(dmabufs.borrow_mut().insert(fd, dmabuf).is_none());
|
||||
}
|
||||
|
||||
@@ -421,7 +622,7 @@ impl PipeWire {
|
||||
let spa_data = (*spa_buffer).datas;
|
||||
assert!((*spa_buffer).n_datas > 0);
|
||||
|
||||
let fd = (*spa_data).fd as i32;
|
||||
let fd = (*spa_data).fd;
|
||||
dmabufs.borrow_mut().remove(&fd);
|
||||
}
|
||||
}
|
||||
@@ -429,10 +630,10 @@ impl PipeWire {
|
||||
.register()
|
||||
.unwrap();
|
||||
|
||||
trace!("starting pw stream with size={pending_size:?}, refresh={refresh}");
|
||||
trace!("starting pw stream with size={pending_size:?}, refresh={refresh:?}");
|
||||
|
||||
let params;
|
||||
make_params!(params, pending_size, refresh, alpha);
|
||||
make_params!(params, &formats, pending_size, refresh.get(), alpha);
|
||||
stream
|
||||
.connect(
|
||||
Direction::Output,
|
||||
@@ -448,13 +649,15 @@ impl PipeWire {
|
||||
_listener: listener,
|
||||
is_active,
|
||||
target,
|
||||
size,
|
||||
formats,
|
||||
state,
|
||||
refresh,
|
||||
offer_alpha: alpha,
|
||||
cursor_mode,
|
||||
last_frame_time: Duration::ZERO,
|
||||
min_time_between_frames,
|
||||
dmabufs,
|
||||
scheduled_redraw: None,
|
||||
};
|
||||
Ok(cast)
|
||||
}
|
||||
@@ -462,12 +665,14 @@ impl PipeWire {
|
||||
|
||||
impl Cast {
|
||||
pub fn ensure_size(&self, size: Size<i32, Physical>) -> anyhow::Result<CastSizeChange> {
|
||||
let current_size = self.size.get();
|
||||
if current_size == CastSize::Ready(size) {
|
||||
let new_size = Size::from((size.w as u32, size.h as u32));
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
if matches!(&*state, CastState::Ready { size, .. } if *size == new_size) {
|
||||
return Ok(CastSizeChange::Ready);
|
||||
}
|
||||
|
||||
if current_size.pending_size() == Some(size) {
|
||||
if state.pending_size() == Some(new_size) {
|
||||
debug!("stream size still hasn't changed, skipping frame");
|
||||
return Ok(CastSizeChange::Pending);
|
||||
}
|
||||
@@ -475,10 +680,18 @@ impl Cast {
|
||||
let _span = tracy_client::span!("Cast::ensure_size");
|
||||
debug!("cast size changed, updating stream size");
|
||||
|
||||
self.size.set(current_size.with_pending(size));
|
||||
*state = CastState::ResizePending {
|
||||
pending_size: new_size,
|
||||
};
|
||||
|
||||
let params;
|
||||
make_params!(params, size, self.refresh, self.offer_alpha);
|
||||
make_params!(
|
||||
params,
|
||||
&self.formats,
|
||||
new_size,
|
||||
self.refresh.get(),
|
||||
self.offer_alpha
|
||||
);
|
||||
self.stream
|
||||
.update_params(params)
|
||||
.context("error updating stream params")?;
|
||||
@@ -487,17 +700,17 @@ impl Cast {
|
||||
}
|
||||
|
||||
pub fn set_refresh(&mut self, refresh: u32) -> anyhow::Result<()> {
|
||||
if self.refresh == refresh {
|
||||
if self.refresh.get() == refresh {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _span = tracy_client::span!("Cast::set_refresh");
|
||||
debug!("cast FPS changed, updating stream FPS");
|
||||
self.refresh = refresh;
|
||||
self.refresh.set(refresh);
|
||||
|
||||
let size = self.size.get().expected_format_size();
|
||||
let size = self.state.borrow().expected_format_size();
|
||||
let params;
|
||||
make_params!(params, size, self.refresh, self.offer_alpha);
|
||||
make_params!(params, &self.formats, size, refresh, self.offer_alpha);
|
||||
self.stream
|
||||
.update_params(params)
|
||||
.context("error updating stream params")?;
|
||||
@@ -505,13 +718,13 @@ impl Cast {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn should_skip_frame(&self, target_frame_time: Duration) -> bool {
|
||||
fn compute_extra_delay(&self, target_frame_time: Duration) -> Duration {
|
||||
let last = self.last_frame_time;
|
||||
let min = self.min_time_between_frames.get();
|
||||
|
||||
if last.is_zero() {
|
||||
trace!(?target_frame_time, ?last, "last is zero, recording");
|
||||
return false;
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
if target_frame_time < last {
|
||||
@@ -521,29 +734,106 @@ impl Cast {
|
||||
?last,
|
||||
"target frame time is below last, did it overflow or did we mispredict?"
|
||||
);
|
||||
return false;
|
||||
return Duration::ZERO;
|
||||
}
|
||||
|
||||
let diff = target_frame_time - last;
|
||||
if diff < min {
|
||||
let delay = min - diff;
|
||||
trace!(
|
||||
?target_frame_time,
|
||||
?last,
|
||||
"skipping frame because it is too soon: diff={diff:?} < min={min:?}",
|
||||
"frame is too soon: min={min:?}, delay={:?}",
|
||||
delay
|
||||
);
|
||||
return true;
|
||||
return delay;
|
||||
} else {
|
||||
trace!("overshoot={:?}", diff - min);
|
||||
}
|
||||
|
||||
false
|
||||
Duration::ZERO
|
||||
}
|
||||
|
||||
fn schedule_redraw(
|
||||
&mut self,
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
output: Output,
|
||||
target_time: Duration,
|
||||
) {
|
||||
if self.scheduled_redraw.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let now = get_monotonic_time();
|
||||
let duration = target_time.saturating_sub(now);
|
||||
let timer = Timer::from_duration(duration);
|
||||
let token = event_loop
|
||||
.insert_source(timer, move |_, _, state| {
|
||||
state.niri.queue_redraw(&output);
|
||||
TimeoutAction::Drop
|
||||
})
|
||||
.unwrap();
|
||||
self.scheduled_redraw = Some(token);
|
||||
}
|
||||
|
||||
fn remove_scheduled_redraw(&mut self, event_loop: &LoopHandle<'static, State>) {
|
||||
if let Some(token) = self.scheduled_redraw.take() {
|
||||
event_loop.remove(token);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether this frame should be skipped because it's too soon.
|
||||
///
|
||||
/// If the frame should be skipped, schedules a redraw and returns `true`. Otherwise, removes a
|
||||
/// scheduled redraw, if any, and returns `false`.
|
||||
///
|
||||
/// When this method returns `false`, the calling code is assumed to follow up with
|
||||
/// [`Cast::dequeue_buffer_and_render()`].
|
||||
pub fn check_time_and_schedule(
|
||||
&mut self,
|
||||
event_loop: &LoopHandle<'static, State>,
|
||||
output: &Output,
|
||||
target_frame_time: Duration,
|
||||
) -> bool {
|
||||
let delay = self.compute_extra_delay(target_frame_time);
|
||||
if delay >= CAST_DELAY_ALLOWANCE {
|
||||
trace!("delay >= allowance, scheduling redraw");
|
||||
self.schedule_redraw(event_loop, output.clone(), target_frame_time + delay);
|
||||
true
|
||||
} else {
|
||||
self.remove_scheduled_redraw(event_loop);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dequeue_buffer_and_render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
) -> bool {
|
||||
let CastState::Ready { damage_tracker, .. } = &mut *self.state.borrow_mut() else {
|
||||
error!("cast must be in Ready state to render");
|
||||
return false;
|
||||
};
|
||||
let damage_tracker = damage_tracker
|
||||
.get_or_insert_with(|| OutputDamageTracker::new(size, scale, Transform::Normal));
|
||||
|
||||
// Size change will drop the damage tracker, but scale change won't, so check it here.
|
||||
let OutputModeSource::Static { scale: t_scale, .. } = damage_tracker.mode() else {
|
||||
unreachable!();
|
||||
};
|
||||
if *t_scale != scale {
|
||||
*damage_tracker = OutputDamageTracker::new(size, scale, Transform::Normal);
|
||||
}
|
||||
|
||||
let (damage, _states) = damage_tracker.damage_output(1, elements).unwrap();
|
||||
if damage.is_none() {
|
||||
trace!("no damage, skipping frame");
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut buffer = match self.stream.dequeue_buffer() {
|
||||
Some(buffer) => buffer,
|
||||
None => {
|
||||
@@ -552,77 +842,88 @@ impl Cast {
|
||||
}
|
||||
};
|
||||
|
||||
let data = &mut buffer.datas_mut()[0];
|
||||
let fd = data.as_raw().fd as i32;
|
||||
let dmabuf = self.dmabufs.borrow()[&fd].clone();
|
||||
let fd = buffer.datas_mut()[0].as_raw().fd;
|
||||
let dmabuf = &self.dmabufs.borrow()[&fd];
|
||||
|
||||
if let Err(err) =
|
||||
render_to_dmabuf(renderer, dmabuf, size, scale, Transform::Normal, elements)
|
||||
{
|
||||
if let Err(err) = render_to_dmabuf(
|
||||
renderer,
|
||||
dmabuf.clone(),
|
||||
size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
elements.iter().rev(),
|
||||
) {
|
||||
warn!("error rendering to dmabuf: {err:?}");
|
||||
return false;
|
||||
}
|
||||
|
||||
let maxsize = data.as_raw().maxsize;
|
||||
let chunk = data.chunk_mut();
|
||||
*chunk.size_mut() = maxsize;
|
||||
*chunk.stride_mut() = maxsize as i32 / size.h;
|
||||
for (data, (stride, offset)) in
|
||||
zip(buffer.datas_mut(), zip(dmabuf.strides(), dmabuf.offsets()))
|
||||
{
|
||||
let chunk = data.chunk_mut();
|
||||
*chunk.size_mut() = 1;
|
||||
*chunk.stride_mut() = stride as i32;
|
||||
*chunk.offset_mut() = offset;
|
||||
|
||||
trace!(
|
||||
"pw buffer: fd = {}, stride = {stride}, offset = {offset}",
|
||||
data.as_raw().fd
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl CastSize {
|
||||
fn pending_size(self) -> Option<Size<i32, Physical>> {
|
||||
impl CastState {
|
||||
fn pending_size(&self) -> Option<Size<u32, Physical>> {
|
||||
match self {
|
||||
CastSize::InitialPending(pending) => Some(pending),
|
||||
CastSize::Ready(_) => None,
|
||||
CastSize::ChangePending { pending, .. } => Some(pending),
|
||||
CastState::ResizePending { pending_size } => Some(*pending_size),
|
||||
CastState::ConfirmationPending { size, .. } => Some(*size),
|
||||
CastState::Ready { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn negotiated_size(self) -> Option<Size<i32, Physical>> {
|
||||
fn expected_format_size(&self) -> Size<u32, Physical> {
|
||||
match self {
|
||||
CastSize::InitialPending(_) => None,
|
||||
CastSize::Ready(size) => Some(size),
|
||||
CastSize::ChangePending {
|
||||
last_negotiated, ..
|
||||
} => Some(last_negotiated),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_format_size(self) -> Size<i32, Physical> {
|
||||
match self {
|
||||
CastSize::InitialPending(pending) => pending,
|
||||
CastSize::Ready(size) => size,
|
||||
CastSize::ChangePending { pending, .. } => pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_pending(self, pending: Size<i32, Physical>) -> Self {
|
||||
match self {
|
||||
CastSize::InitialPending(_) => CastSize::InitialPending(pending),
|
||||
CastSize::Ready(size) => CastSize::ChangePending {
|
||||
last_negotiated: size,
|
||||
pending,
|
||||
},
|
||||
CastSize::ChangePending {
|
||||
last_negotiated, ..
|
||||
} => CastSize::ChangePending {
|
||||
last_negotiated,
|
||||
pending,
|
||||
},
|
||||
CastState::ResizePending { pending_size } => *pending_size,
|
||||
CastState::ConfirmationPending { size, .. } => *size,
|
||||
CastState::Ready { size, .. } => *size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_video_params(size: Size<i32, Physical>, refresh: u32, alpha: bool) -> pod::Object {
|
||||
fn make_video_params(
|
||||
formats: &FormatSet,
|
||||
size: Size<u32, Physical>,
|
||||
refresh: u32,
|
||||
alpha: bool,
|
||||
) -> pod::Object {
|
||||
let format = if alpha {
|
||||
VideoFormat::BGRA
|
||||
} else {
|
||||
VideoFormat::BGRx
|
||||
};
|
||||
|
||||
let fourcc = if alpha {
|
||||
Fourcc::Argb8888
|
||||
} else {
|
||||
Fourcc::Xrgb8888
|
||||
};
|
||||
|
||||
let formats: Vec<_> = formats
|
||||
.iter()
|
||||
.filter_map(|f| (f.code == fourcc).then_some(u64::from(f.modifier) as i64))
|
||||
.collect();
|
||||
|
||||
trace!("offering: {formats:?}");
|
||||
|
||||
let dont_fixate = if formats.len() > 1 {
|
||||
PropertyFlags::DONT_FIXATE
|
||||
} else {
|
||||
PropertyFlags::empty()
|
||||
};
|
||||
|
||||
pod::object!(
|
||||
SpaTypes::ObjectParamFormat,
|
||||
ParamType::EnumFormat,
|
||||
@@ -631,15 +932,21 @@ fn make_video_params(size: Size<i32, Physical>, refresh: u32, alpha: bool) -> po
|
||||
pod::property!(FormatProperties::VideoFormat, Id, format),
|
||||
Property {
|
||||
key: FormatProperties::VideoModifier.as_raw(),
|
||||
value: pod::Value::Long(u64::from(Modifier::Invalid) as i64),
|
||||
flags: PropertyFlags::MANDATORY,
|
||||
flags: PropertyFlags::MANDATORY | dont_fixate,
|
||||
value: pod::Value::Choice(ChoiceValue::Long(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Enum {
|
||||
default: formats[0],
|
||||
alternatives: formats,
|
||||
}
|
||||
)))
|
||||
},
|
||||
pod::property!(
|
||||
FormatProperties::VideoSize,
|
||||
Rectangle,
|
||||
Rectangle {
|
||||
width: size.w as u32,
|
||||
height: size.h as u32,
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
}
|
||||
),
|
||||
pod::property!(
|
||||
@@ -669,3 +976,68 @@ fn make_pod(buffer: &mut Vec<u8>, object: pod::Object) -> &Pod {
|
||||
PodSerializer::serialize(Cursor::new(&mut *buffer), &pod::Value::Object(object)).unwrap();
|
||||
Pod::from_bytes(buffer).unwrap()
|
||||
}
|
||||
|
||||
fn find_preferred_modifier(
|
||||
gbm: &GbmDevice<DrmDeviceFd>,
|
||||
size: Size<u32, Physical>,
|
||||
fourcc: Fourcc,
|
||||
modifiers: Vec<i64>,
|
||||
) -> anyhow::Result<(Modifier, usize)> {
|
||||
debug!("find_preferred_modifier: size={size:?}, fourcc={fourcc}, modifiers={modifiers:?}");
|
||||
|
||||
let (buffer, modifier) = allocate_buffer(gbm, size, fourcc, &modifiers)?;
|
||||
|
||||
let dmabuf = buffer
|
||||
.export()
|
||||
.context("error exporting GBM buffer object as dmabuf")?;
|
||||
let plane_count = dmabuf.num_planes();
|
||||
|
||||
// FIXME: Ideally this also needs to try binding the dmabuf for rendering.
|
||||
|
||||
Ok((modifier, plane_count))
|
||||
}
|
||||
|
||||
fn allocate_buffer(
|
||||
gbm: &GbmDevice<DrmDeviceFd>,
|
||||
size: Size<u32, Physical>,
|
||||
fourcc: Fourcc,
|
||||
modifiers: &[i64],
|
||||
) -> anyhow::Result<(GbmBuffer, Modifier)> {
|
||||
let (w, h) = (size.w, size.h);
|
||||
let flags = GbmBufferFlags::RENDERING;
|
||||
|
||||
if modifiers.len() == 1 && Modifier::from(modifiers[0] as u64) == Modifier::Invalid {
|
||||
let bo = gbm
|
||||
.create_buffer_object::<()>(w, h, fourcc, flags)
|
||||
.context("error creating GBM buffer object")?;
|
||||
|
||||
let buffer = GbmBuffer::from_bo(bo, true);
|
||||
Ok((buffer, Modifier::Invalid))
|
||||
} else {
|
||||
let modifiers = modifiers
|
||||
.iter()
|
||||
.map(|m| Modifier::from(*m as u64))
|
||||
.filter(|m| *m != Modifier::Invalid);
|
||||
|
||||
let bo = gbm
|
||||
.create_buffer_object_with_modifiers2::<()>(w, h, fourcc, modifiers, flags)
|
||||
.context("error creating GBM buffer object")?;
|
||||
|
||||
let modifier = bo.modifier().unwrap();
|
||||
let buffer = GbmBuffer::from_bo(bo, false);
|
||||
Ok((buffer, modifier))
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_dmabuf(
|
||||
gbm: &GbmDevice<DrmDeviceFd>,
|
||||
size: Size<u32, Physical>,
|
||||
fourcc: Fourcc,
|
||||
modifier: Modifier,
|
||||
) -> anyhow::Result<Dmabuf> {
|
||||
let (buffer, _modifier) = allocate_buffer(gbm, size, fourcc, &[u64::from(modifier) as i64])?;
|
||||
let dmabuf = buffer
|
||||
.export()
|
||||
.context("error exporting GBM buffer object as dmabuf")?;
|
||||
Ok(dmabuf)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
use niri_config::CornerRadius;
|
||||
use niri_config::{
|
||||
Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation,
|
||||
};
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
|
||||
@@ -28,8 +30,9 @@ pub struct BorderRenderElement {
|
||||
struct Parameters {
|
||||
size: Size<f64, Logical>,
|
||||
gradient_area: Rectangle<f64, Logical>,
|
||||
color_from: [f32; 4],
|
||||
color_to: [f32; 4],
|
||||
gradient_format: GradientInterpolation,
|
||||
color_from: Color,
|
||||
color_to: Color,
|
||||
angle: f32,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
border_width: f32,
|
||||
@@ -43,8 +46,9 @@ impl BorderRenderElement {
|
||||
pub fn new(
|
||||
size: Size<f64, Logical>,
|
||||
gradient_area: Rectangle<f64, Logical>,
|
||||
color_from: [f32; 4],
|
||||
color_to: [f32; 4],
|
||||
gradient_format: GradientInterpolation,
|
||||
color_from: Color,
|
||||
color_to: Color,
|
||||
angle: f32,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
border_width: f32,
|
||||
@@ -57,6 +61,7 @@ impl BorderRenderElement {
|
||||
params: Parameters {
|
||||
size,
|
||||
gradient_area,
|
||||
gradient_format,
|
||||
color_from,
|
||||
color_to,
|
||||
angle,
|
||||
@@ -77,6 +82,7 @@ impl BorderRenderElement {
|
||||
params: Parameters {
|
||||
size: Default::default(),
|
||||
gradient_area: Default::default(),
|
||||
gradient_format: GradientInterpolation::default(),
|
||||
color_from: Default::default(),
|
||||
color_to: Default::default(),
|
||||
angle: 0.,
|
||||
@@ -97,8 +103,9 @@ impl BorderRenderElement {
|
||||
&mut self,
|
||||
size: Size<f64, Logical>,
|
||||
gradient_area: Rectangle<f64, Logical>,
|
||||
color_from: [f32; 4],
|
||||
color_to: [f32; 4],
|
||||
gradient_format: GradientInterpolation,
|
||||
color_from: Color,
|
||||
color_to: Color,
|
||||
angle: f32,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
border_width: f32,
|
||||
@@ -108,6 +115,7 @@ impl BorderRenderElement {
|
||||
let params = Parameters {
|
||||
size,
|
||||
gradient_area,
|
||||
gradient_format,
|
||||
color_from,
|
||||
color_to,
|
||||
angle,
|
||||
@@ -128,6 +136,7 @@ impl BorderRenderElement {
|
||||
let Parameters {
|
||||
size,
|
||||
gradient_area,
|
||||
gradient_format,
|
||||
color_from,
|
||||
color_to,
|
||||
angle,
|
||||
@@ -150,7 +159,7 @@ impl BorderRenderElement {
|
||||
}
|
||||
|
||||
let mut grad_vec = grad_area_diag.project_onto(grad_dir);
|
||||
if grad_dir.y <= 0. {
|
||||
if grad_dir.y < 0. {
|
||||
grad_vec = -grad_vec;
|
||||
}
|
||||
|
||||
@@ -162,13 +171,29 @@ impl BorderRenderElement {
|
||||
let input_to_geo =
|
||||
Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size);
|
||||
|
||||
let colorspace = match gradient_format.color_space {
|
||||
GradientColorSpace::Srgb => 0.,
|
||||
GradientColorSpace::SrgbLinear => 1.,
|
||||
GradientColorSpace::Oklab => 2.,
|
||||
GradientColorSpace::Oklch => 3.,
|
||||
};
|
||||
|
||||
let hue_interpolation = match gradient_format.hue_interpolation {
|
||||
HueInterpolation::Shorter => 0.,
|
||||
HueInterpolation::Longer => 1.,
|
||||
HueInterpolation::Increasing => 2.,
|
||||
HueInterpolation::Decreasing => 3.,
|
||||
};
|
||||
|
||||
self.inner.update(
|
||||
size,
|
||||
None,
|
||||
scale,
|
||||
vec![
|
||||
Uniform::new("color_from", color_from),
|
||||
Uniform::new("color_to", color_to),
|
||||
Uniform::new("colorspace", colorspace),
|
||||
Uniform::new("hue_interpolation", hue_interpolation),
|
||||
Uniform::new("color_from", color_from.to_array_unpremul()),
|
||||
Uniform::new("color_to", color_to.to_array_unpremul()),
|
||||
Uniform::new("grad_offset", grad_offset.to_array()),
|
||||
Uniform::new("grad_width", w),
|
||||
Uniform::new("grad_vec", grad_vec.to_array()),
|
||||
|
||||
+19
-19
@@ -2,12 +2,13 @@ use std::ptr;
|
||||
|
||||
use anyhow::{ensure, Context};
|
||||
use niri_config::BlockOutFrom;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::{Buffer, Fourcc};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Kind, RenderElement};
|
||||
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::{buffer_dimensions, Bind, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
@@ -233,16 +234,19 @@ pub fn render_to_vec(
|
||||
Ok(copy.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
pub fn render_to_dmabuf(
|
||||
renderer: &mut GlesRenderer,
|
||||
dmabuf: smithay::backend::allocator::dmabuf::Dmabuf,
|
||||
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!();
|
||||
ensure!(
|
||||
dmabuf.width() == size.w as u32 && dmabuf.height() == size.h as u32,
|
||||
"invalid buffer size"
|
||||
);
|
||||
renderer.bind(dmabuf).context("error binding texture")?;
|
||||
render_elements(renderer, size, scale, transform, elements)
|
||||
}
|
||||
@@ -250,32 +254,28 @@ pub fn render_to_dmabuf(
|
||||
pub fn render_to_shm(
|
||||
renderer: &mut GlesRenderer,
|
||||
buffer: &WlBuffer,
|
||||
size: Size<i32, Physical>,
|
||||
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.format == wl_shm::Format::Xrgb8888
|
||||
&& buffer_data.width == size.w
|
||||
&& buffer_data.height == size.h
|
||||
&& shm_len as i32 == buffer_data.stride * buffer_data.height,
|
||||
&& buffer_data.stride == size.w * 4
|
||||
&& shm_len == buffer_data.stride as usize * buffer_data.height as usize,
|
||||
"invalid buffer format or size"
|
||||
);
|
||||
let mapping =
|
||||
render_and_download(renderer, size, scale, transform, Fourcc::Xrgb8888, elements)?;
|
||||
|
||||
ensure!(bytes.len() == shm_len, "mapped buffer has wrong length");
|
||||
let bytes = renderer
|
||||
.map_texture(&mapping)
|
||||
.context("error mapping texture")?;
|
||||
|
||||
unsafe {
|
||||
let _span = tracy_client::span!("copy_nonoverlapping");
|
||||
@@ -302,7 +302,7 @@ fn render_elements(
|
||||
.context("error starting frame")?;
|
||||
|
||||
frame
|
||||
.clear([0., 0., 0., 0.], &[output_rect])
|
||||
.clear(Color32F::TRANSPARENT, &[output_rect])
|
||||
.context("error clearing")?;
|
||||
|
||||
for element in elements {
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::texture::TextureRenderElement;
|
||||
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
|
||||
|
||||
/// Wrapper for a texture from the primary GPU for rendering with the primary GPU.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrimaryGpuTextureRenderElement(pub TextureRenderElement<GlesTexture>);
|
||||
|
||||
impl Element for PrimaryGpuTextureRenderElement {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub trait NiriRenderer:
|
||||
+ AsGlesRenderer
|
||||
{
|
||||
// Associated types to work around the instability of associated type bounds.
|
||||
type NiriTextureId: Texture + Clone + 'static;
|
||||
type NiriTextureId: Texture + Clone + Send + 'static;
|
||||
type NiriError: std::error::Error
|
||||
+ Send
|
||||
+ Sync
|
||||
@@ -28,7 +28,7 @@ pub trait NiriRenderer:
|
||||
impl<R> NiriRenderer for R
|
||||
where
|
||||
R: ImportAll + ImportMem + ExportMem + Bind<Dmabuf> + Offscreen<GlesTexture> + AsGlesRenderer,
|
||||
R::TextureId: Texture + Clone + 'static,
|
||||
R::TextureId: Texture + Clone + Send + 'static,
|
||||
R::Error: std::error::Error + Send + Sync + From<<GlesRenderer as Renderer>::Error> + 'static,
|
||||
{
|
||||
type NiriTextureId = R::TextureId;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ffi::CString;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::{Mat3, Vec2};
|
||||
@@ -76,38 +76,31 @@ unsafe fn compile_program(
|
||||
let debug_program =
|
||||
unsafe { link_program(gl, include_str!("shaders/texture.vert"), &debug_shader)? };
|
||||
|
||||
let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated");
|
||||
let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated");
|
||||
let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated");
|
||||
let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated");
|
||||
let size = CStr::from_bytes_with_nul(b"niri_size\0").expect("NULL terminated");
|
||||
let scale = CStr::from_bytes_with_nul(b"niri_scale\0").expect("NULL terminated");
|
||||
let alpha = CStr::from_bytes_with_nul(b"niri_alpha\0").expect("NULL terminated");
|
||||
let tint = CStr::from_bytes_with_nul(b"niri_tint\0").expect("NULL terminated");
|
||||
let vert = c"vert";
|
||||
let vert_position = c"vert_position";
|
||||
let matrix = c"matrix";
|
||||
let tex_matrix = c"tex_matrix";
|
||||
let size = c"niri_size";
|
||||
let scale = c"niri_scale";
|
||||
let alpha = c"niri_alpha";
|
||||
let tint = c"niri_tint";
|
||||
|
||||
Ok(ShaderProgram(Rc::new(ShaderProgramInner {
|
||||
normal: ShaderProgramInternal {
|
||||
program,
|
||||
uniform_matrix: gl
|
||||
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_tex_matrix: gl
|
||||
.GetUniformLocation(program, tex_matrix.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_size: gl
|
||||
.GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_scale: gl
|
||||
.GetUniformLocation(program, scale.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_alpha: gl
|
||||
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert_position: gl
|
||||
.GetAttribLocation(program, vert_position.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_matrix: gl.GetUniformLocation(program, matrix.as_ptr()),
|
||||
uniform_tex_matrix: gl.GetUniformLocation(program, tex_matrix.as_ptr()),
|
||||
uniform_size: gl.GetUniformLocation(program, size.as_ptr()),
|
||||
uniform_scale: gl.GetUniformLocation(program, scale.as_ptr()),
|
||||
uniform_alpha: gl.GetUniformLocation(program, alpha.as_ptr()),
|
||||
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()),
|
||||
attrib_vert_position: gl.GetAttribLocation(program, vert_position.as_ptr()),
|
||||
additional_uniforms: additional_uniforms
|
||||
.iter()
|
||||
.map(|uniform| {
|
||||
let name =
|
||||
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
|
||||
let location =
|
||||
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
|
||||
let location = gl.GetUniformLocation(program, name.as_ptr());
|
||||
(
|
||||
uniform.name.clone().into_owned(),
|
||||
UniformDesc {
|
||||
@@ -121,41 +114,26 @@ unsafe fn compile_program(
|
||||
.iter()
|
||||
.map(|name_| {
|
||||
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
|
||||
let location =
|
||||
gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar);
|
||||
let location = gl.GetUniformLocation(program, name.as_ptr());
|
||||
(name_.to_string(), location)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
debug: ShaderProgramInternal {
|
||||
program: debug_program,
|
||||
uniform_matrix: gl
|
||||
.GetUniformLocation(debug_program, matrix.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_tex_matrix: gl.GetUniformLocation(
|
||||
debug_program,
|
||||
tex_matrix.as_ptr() as *const ffi::types::GLchar,
|
||||
),
|
||||
uniform_size: gl
|
||||
.GetUniformLocation(debug_program, size.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_scale: gl
|
||||
.GetUniformLocation(debug_program, scale.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_alpha: gl
|
||||
.GetUniformLocation(debug_program, alpha.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert: gl
|
||||
.GetAttribLocation(debug_program, vert.as_ptr() as *const ffi::types::GLchar),
|
||||
attrib_vert_position: gl.GetAttribLocation(
|
||||
debug_program,
|
||||
vert_position.as_ptr() as *const ffi::types::GLchar,
|
||||
),
|
||||
uniform_matrix: gl.GetUniformLocation(debug_program, matrix.as_ptr()),
|
||||
uniform_tex_matrix: gl.GetUniformLocation(debug_program, tex_matrix.as_ptr()),
|
||||
uniform_size: gl.GetUniformLocation(debug_program, size.as_ptr()),
|
||||
uniform_scale: gl.GetUniformLocation(debug_program, scale.as_ptr()),
|
||||
uniform_alpha: gl.GetUniformLocation(debug_program, alpha.as_ptr()),
|
||||
attrib_vert: gl.GetAttribLocation(debug_program, vert.as_ptr()),
|
||||
attrib_vert_position: gl.GetAttribLocation(debug_program, vert_position.as_ptr()),
|
||||
additional_uniforms: additional_uniforms
|
||||
.iter()
|
||||
.map(|uniform| {
|
||||
let name =
|
||||
CString::new(uniform.name.as_bytes()).expect("Interior null in name");
|
||||
let location = gl.GetUniformLocation(
|
||||
debug_program,
|
||||
name.as_ptr() as *const ffi::types::GLchar,
|
||||
);
|
||||
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
|
||||
(
|
||||
uniform.name.clone().into_owned(),
|
||||
UniformDesc {
|
||||
@@ -169,16 +147,12 @@ unsafe fn compile_program(
|
||||
.iter()
|
||||
.map(|name_| {
|
||||
let name = CString::new(name_.as_bytes()).expect("Interior null in name");
|
||||
let location = gl.GetUniformLocation(
|
||||
debug_program,
|
||||
name.as_ptr() as *const ffi::types::GLchar,
|
||||
);
|
||||
let location = gl.GetUniformLocation(debug_program, name.as_ptr());
|
||||
(name_.to_string(), location)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
uniform_tint: gl
|
||||
.GetUniformLocation(debug_program, tint.as_ptr() as *const ffi::types::GLchar),
|
||||
uniform_tint: gl.GetUniformLocation(debug_program, tint.as_ptr()),
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -211,7 +185,7 @@ impl ShaderRenderElement {
|
||||
// Should only be used for visual improvements, i.e. corner radius anti-aliasing.
|
||||
scale: f32,
|
||||
alpha: f32,
|
||||
uniforms: Vec<Uniform<'_>>,
|
||||
additional_uniforms: Vec<Uniform<'static>>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
@@ -223,7 +197,7 @@ impl ShaderRenderElement {
|
||||
opaque_regions: opaque_regions.unwrap_or_default(),
|
||||
scale,
|
||||
alpha,
|
||||
additional_uniforms: uniforms.into_iter().map(|u| u.into_owned()).collect(),
|
||||
additional_uniforms,
|
||||
textures,
|
||||
kind,
|
||||
}
|
||||
@@ -253,13 +227,13 @@ impl ShaderRenderElement {
|
||||
size: Size<f64, Logical>,
|
||||
opaque_regions: Option<Vec<Rectangle<f64, Logical>>>,
|
||||
scale: f32,
|
||||
uniforms: Vec<Uniform<'_>>,
|
||||
uniforms: Vec<Uniform<'static>>,
|
||||
textures: HashMap<String, GlesTexture>,
|
||||
) {
|
||||
self.area.size = size;
|
||||
self.opaque_regions = opaque_regions.unwrap_or_default();
|
||||
self.scale = scale;
|
||||
self.additional_uniforms = uniforms.into_iter().map(|u| u.into_owned()).collect();
|
||||
self.additional_uniforms = uniforms;
|
||||
self.textures = textures;
|
||||
|
||||
self.commit_counter.increment();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
precision mediump float;
|
||||
precision highp float;
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
uniform float niri_tint;
|
||||
@@ -10,6 +10,8 @@ uniform float niri_scale;
|
||||
uniform vec2 niri_size;
|
||||
varying vec2 niri_v_coords;
|
||||
|
||||
uniform float colorspace;
|
||||
uniform float hue_interpolation;
|
||||
uniform vec4 color_from;
|
||||
uniform vec4 color_to;
|
||||
uniform vec2 grad_offset;
|
||||
@@ -21,6 +23,176 @@ uniform vec2 geo_size;
|
||||
uniform vec4 outer_radius;
|
||||
uniform float border_width;
|
||||
|
||||
vec4 premul_rect(vec4 color) {
|
||||
color.rgb *= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
vec4 premul_lch(vec4 color) {
|
||||
color.xy *= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
vec4 unpremul_rect(vec4 color) {
|
||||
if (color.a == 0.0)
|
||||
return color;
|
||||
|
||||
color.rgb /= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
vec4 unpremul_lch(vec4 color) {
|
||||
if (color.a == 0.0)
|
||||
return color;
|
||||
|
||||
color.xy /= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
vec4 premul_mix_unpremul_rect(vec4 color1, vec4 color2, float ratio) {
|
||||
vec4 mixed = mix(premul_rect(color1), premul_rect(color2), ratio);
|
||||
return unpremul_rect(mixed);
|
||||
}
|
||||
|
||||
vec4 premul_mix_unpremul_lch(vec4 color1, vec4 color2, float ratio) {
|
||||
vec4 mixed = mix(premul_lch(color1), premul_lch(color2), ratio);
|
||||
return unpremul_lch(mixed);
|
||||
}
|
||||
|
||||
vec3 srgb_to_linear(vec3 color) {
|
||||
return pow(color, vec3(2.2));
|
||||
}
|
||||
|
||||
vec3 linear_to_srgb(vec3 color) {
|
||||
return pow(color, vec3(1.0 / 2.2));
|
||||
}
|
||||
|
||||
vec3 lab_to_lch(vec3 color) {
|
||||
float c = sqrt(pow(color.y, 2.0) + pow(color.z, 2.0));
|
||||
float h = degrees(atan(color.z, color.y)) ;
|
||||
h += h <= 0.0 ?
|
||||
360.0 :
|
||||
0.0 ;
|
||||
return vec3(
|
||||
color.x,
|
||||
c,
|
||||
h
|
||||
);
|
||||
}
|
||||
|
||||
vec3 lch_to_lab(vec3 color) {
|
||||
float a = color.y * clamp(cos(radians(color.z)), -1.0, 1.0);
|
||||
float b = color.y * clamp(sin(radians(color.z)), -1.0, 1.0);
|
||||
return vec3(
|
||||
color.x,
|
||||
a,
|
||||
b
|
||||
);
|
||||
}
|
||||
|
||||
vec3 linear_to_oklab(vec3 color){
|
||||
mat3 rgb_to_lms = mat3(
|
||||
vec3(0.4122214708, 0.5363325363, 0.0514459929),
|
||||
vec3(0.2119034982, 0.6806995451, 0.1073969566),
|
||||
vec3(0.0883024619, 0.2817188376, 0.6299787005)
|
||||
);
|
||||
mat3 lms_to_oklab = mat3(
|
||||
vec3(0.2104542553, 0.7936177850, -0.0040720468),
|
||||
vec3(1.9779984951, -2.4285922050, 0.4505937099),
|
||||
vec3(0.0259040371, 0.7827717662, -0.8086757660)
|
||||
);
|
||||
vec3 lms = color * rgb_to_lms;
|
||||
lms = pow(lms, vec3(1.0 / 3.0));
|
||||
return lms * lms_to_oklab;
|
||||
}
|
||||
|
||||
vec3 oklab_to_linear(vec3 color){
|
||||
mat3 oklab_to_lms = mat3(
|
||||
vec3(1.0, 0.3963377774, 0.2158037573),
|
||||
vec3(1.0, -0.1055613458, -0.0638541728),
|
||||
vec3(1.0, -0.0894841775, -1.2914855480)
|
||||
);
|
||||
mat3 lms_to_rgb = mat3(
|
||||
vec3(4.0767416621, -3.3077115913, 0.2309699292),
|
||||
vec3(-1.2684380046, 2.6097574011, -0.3413193965),
|
||||
vec3(-0.0041960863, -0.7034186147, 1.7076147010)
|
||||
);
|
||||
vec3 lms = color * oklab_to_lms;
|
||||
lms = pow(lms, vec3(3.0));
|
||||
return lms * lms_to_rgb;
|
||||
}
|
||||
|
||||
vec4 color_mix(vec4 color1, vec4 color2, float color_ratio) {
|
||||
vec4 color_out;
|
||||
|
||||
// srgb
|
||||
if (colorspace == 0.0) {
|
||||
return mix(premul_rect(color1), premul_rect(color2), color_ratio);
|
||||
}
|
||||
|
||||
color1.rgb = srgb_to_linear(color1.rgb);
|
||||
color2.rgb = srgb_to_linear(color2.rgb);
|
||||
|
||||
// srgb-linear
|
||||
if (colorspace == 1.0) {
|
||||
color_out = premul_mix_unpremul_rect(color1, color2, color_ratio);
|
||||
// oklab
|
||||
} else if (colorspace == 2.0) {
|
||||
color1.xyz = linear_to_oklab(color1.rgb);
|
||||
color2.xyz = linear_to_oklab(color2.rgb);
|
||||
color_out = premul_mix_unpremul_rect(color1, color2, color_ratio);
|
||||
color_out.rgb = oklab_to_linear(color_out.xyz);
|
||||
// oklch
|
||||
} else if (colorspace == 3.0) {
|
||||
color1.xyz = lab_to_lch(linear_to_oklab(color1.rgb));
|
||||
color2.xyz = lab_to_lch(linear_to_oklab(color2.rgb));
|
||||
color_out = premul_mix_unpremul_lch(color1, color2, color_ratio);
|
||||
|
||||
float min_hue = min(color1.z, color2.z);
|
||||
float max_hue = max(color1.z, color2.z);
|
||||
float path_direct_distance = (max_hue - min_hue) * color_ratio;
|
||||
float path_mod_distance = (360.0 - max_hue + min_hue) * color_ratio;
|
||||
|
||||
float path_mod =
|
||||
color1.z == min_hue ?
|
||||
mod(color1.z - path_mod_distance, 360.0) :
|
||||
mod(color1.z + path_mod_distance, 360.0) ;
|
||||
float path_direct =
|
||||
color1.z == min_hue ?
|
||||
color1.z + path_direct_distance :
|
||||
color1.z - path_direct_distance ;
|
||||
|
||||
// shorter
|
||||
if (hue_interpolation == 0.0) {
|
||||
color_out.z =
|
||||
max_hue - min_hue > 360.0 - max_hue + min_hue ?
|
||||
path_mod :
|
||||
path_direct ;
|
||||
// longer
|
||||
} else if (hue_interpolation == 1.0) {
|
||||
color_out.z =
|
||||
max_hue - min_hue <= 360.0 - max_hue + min_hue ?
|
||||
path_mod :
|
||||
path_direct ;
|
||||
// increasing
|
||||
} else if (hue_interpolation == 2.0) {
|
||||
color_out.z =
|
||||
color1.z > color2.z ?
|
||||
path_mod :
|
||||
path_direct ;
|
||||
// decreasing
|
||||
} else if (hue_interpolation == 3.0) {
|
||||
color_out.z =
|
||||
color1.z <= color2.z ?
|
||||
path_mod :
|
||||
path_direct ;
|
||||
}
|
||||
color_out.rgb = clamp(oklab_to_linear(lch_to_lab(color_out.xyz)), 0.0, 1.0);
|
||||
}
|
||||
|
||||
return premul_rect(vec4(linear_to_srgb(color_out.rgb), color_out.a));
|
||||
}
|
||||
|
||||
vec4 gradient_color(vec2 coords) {
|
||||
coords = coords + grad_offset;
|
||||
|
||||
@@ -33,7 +205,7 @@ vec4 gradient_color(vec2 coords) {
|
||||
frac += 1.0;
|
||||
|
||||
frac = clamp(frac, 0.0, 1.0);
|
||||
return mix(color_from, color_to, frac);
|
||||
return color_mix(color_from, color_to, frac);
|
||||
}
|
||||
|
||||
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
#endif
|
||||
|
||||
precision mediump float;
|
||||
precision highp float;
|
||||
#if defined(EXTERNAL)
|
||||
uniform samplerExternalOES tex;
|
||||
#else
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
precision mediump float;
|
||||
precision highp float;
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
uniform float niri_tint;
|
||||
|
||||
@@ -34,6 +34,8 @@ impl Shaders {
|
||||
renderer,
|
||||
include_str!("border.frag"),
|
||||
&[
|
||||
UniformName::new("colorspace", UniformType::_1f),
|
||||
UniformName::new("hue_interpolation", UniformType::_1f),
|
||||
UniformName::new("color_from", UniformType::_4f),
|
||||
UniformName::new("color_to", UniformType::_4f),
|
||||
UniformName::new("grad_offset", UniformType::_2f),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
precision mediump float;
|
||||
precision highp float;
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
uniform float niri_tint;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
precision mediump float;
|
||||
precision highp float;
|
||||
|
||||
#if defined(DEBUG_FLAGS)
|
||||
uniform float niri_tint;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
|
||||
use smithay::backend::renderer::{Frame as _, Renderer};
|
||||
use smithay::backend::renderer::{Color32F, Frame as _, Renderer};
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
|
||||
|
||||
/// Smithay's solid color buffer, but with fractional scale.
|
||||
@@ -9,7 +9,7 @@ pub struct SolidColorBuffer {
|
||||
id: Id,
|
||||
size: Size<f64, Logical>,
|
||||
commit: CommitCounter,
|
||||
color: [f32; 4],
|
||||
color: Color32F,
|
||||
}
|
||||
|
||||
/// Render element for a [`SolidColorBuffer`].
|
||||
@@ -18,7 +18,7 @@ pub struct SolidColorRenderElement {
|
||||
id: Id,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
commit: CommitCounter,
|
||||
color: [f32; 4],
|
||||
color: Color32F,
|
||||
kind: Kind,
|
||||
}
|
||||
|
||||
@@ -34,10 +34,10 @@ impl Default for SolidColorBuffer {
|
||||
}
|
||||
|
||||
impl SolidColorBuffer {
|
||||
pub fn new(size: impl Into<Size<f64, Logical>>, color: [f32; 4]) -> Self {
|
||||
pub fn new(size: impl Into<Size<f64, Logical>>, color: impl Into<Color32F>) -> Self {
|
||||
SolidColorBuffer {
|
||||
id: Id::new(),
|
||||
color,
|
||||
color: color.into(),
|
||||
commit: CommitCounter::default(),
|
||||
size: size.into(),
|
||||
}
|
||||
@@ -51,15 +51,17 @@ impl SolidColorBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_color(&mut self, color: [f32; 4]) {
|
||||
pub fn set_color(&mut self, color: impl Into<Color32F>) {
|
||||
let color = color.into();
|
||||
if color != self.color {
|
||||
self.color = color;
|
||||
self.commit.increment();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, size: impl Into<Size<f64, Logical>>, color: [f32; 4]) {
|
||||
pub fn update(&mut self, size: impl Into<Size<f64, Logical>>, color: impl Into<Color32F>) {
|
||||
let size = size.into();
|
||||
let color = color.into();
|
||||
if size != self.size || color != self.color {
|
||||
self.size = size;
|
||||
self.color = color;
|
||||
@@ -67,7 +69,7 @@ impl SolidColorBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(&self) -> [f32; 4] {
|
||||
pub fn color(&self) -> Color32F {
|
||||
self.color
|
||||
}
|
||||
|
||||
@@ -84,12 +86,7 @@ impl SolidColorRenderElement {
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
let geo = Rectangle::from_loc_and_size(location, buffer.size());
|
||||
let color = [
|
||||
buffer.color[0] * alpha,
|
||||
buffer.color[1] * alpha,
|
||||
buffer.color[2] * alpha,
|
||||
buffer.color[3] * alpha,
|
||||
];
|
||||
let color = buffer.color * alpha;
|
||||
Self::new(buffer.id.clone(), geo, buffer.commit, color, kind)
|
||||
}
|
||||
|
||||
@@ -97,7 +94,7 @@ impl SolidColorRenderElement {
|
||||
id: Id,
|
||||
geometry: Rectangle<f64, Logical>,
|
||||
commit: CommitCounter,
|
||||
color: [f32; 4],
|
||||
color: Color32F,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
SolidColorRenderElement {
|
||||
@@ -109,7 +106,7 @@ impl SolidColorRenderElement {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(&self) -> [f32; 4] {
|
||||
pub fn color(&self) -> Color32F {
|
||||
self.color
|
||||
}
|
||||
|
||||
@@ -136,7 +133,7 @@ impl Element for SolidColorRenderElement {
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
if self.color[3] == 1f32 {
|
||||
if self.color.is_opaque() {
|
||||
let rect = Rectangle::from_loc_and_size((0., 0.), self.geometry.size)
|
||||
.to_physical_precise_down(scale);
|
||||
OpaqueRegions::from_slice(&[rect])
|
||||
@@ -146,7 +143,7 @@ impl Element for SolidColorRenderElement {
|
||||
}
|
||||
|
||||
fn alpha(&self) -> f32 {
|
||||
1.0
|
||||
self.color.a()
|
||||
}
|
||||
|
||||
fn kind(&self) -> Kind {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn render_snapshot_from_surface_tree(
|
||||
let data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||
|
||||
if let Some(data) = data {
|
||||
let data = &*data.borrow();
|
||||
let data = &*data.lock().unwrap();
|
||||
|
||||
if let Some(view) = data.view() {
|
||||
location += view.offset.to_f64();
|
||||
@@ -42,19 +42,17 @@ pub fn render_snapshot_from_surface_tree(
|
||||
let data = states.data_map.get::<RendererSurfaceStateUserData>();
|
||||
|
||||
if let Some(data) = data {
|
||||
if let Some(view) = data.borrow().view() {
|
||||
location += view.offset.to_f64();
|
||||
} else {
|
||||
let Some(view) = data.lock().unwrap().view() else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
location += view.offset.to_f64();
|
||||
|
||||
if let Err(err) = import_surface(renderer, states) {
|
||||
warn!("failed to import surface: {err:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let data = data.borrow();
|
||||
let view = data.view().unwrap();
|
||||
let data = data.lock().unwrap();
|
||||
let Some(texture) = data.texture::<GlesRenderer>(renderer.id()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct TextureBuffer<T> {
|
||||
}
|
||||
|
||||
/// Render element for a [`TextureBuffer`].
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureRenderElement<T> {
|
||||
buffer: TextureBuffer<T>,
|
||||
location: Point<f64, Logical>,
|
||||
@@ -92,6 +92,18 @@ impl<T> TextureBuffer<T> {
|
||||
pub fn texture_scale(&self) -> Scale<f64> {
|
||||
self.scale
|
||||
}
|
||||
|
||||
pub fn set_texture_scale(&mut self, scale: impl Into<Scale<f64>>) {
|
||||
self.scale = scale.into();
|
||||
}
|
||||
|
||||
pub fn texture_transform(&self) -> Transform {
|
||||
self.transform
|
||||
}
|
||||
|
||||
pub fn set_texture_transform(&mut self, transform: Transform) {
|
||||
self.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Texture> TextureBuffer<T> {
|
||||
@@ -121,6 +133,10 @@ impl<T> TextureRenderElement<T> {
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &TextureBuffer<T> {
|
||||
&self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Texture> TextureRenderElement<T> {
|
||||
|
||||
@@ -60,7 +60,8 @@ impl ConfigErrorNotification {
|
||||
Animation::new(from, to, 0., c.animations.config_notification_open_close.0)
|
||||
}
|
||||
|
||||
pub fn show_created(&mut self, created_path: Option<PathBuf>) {
|
||||
pub fn show_created(&mut self, created_path: PathBuf) {
|
||||
let created_path = Some(created_path);
|
||||
if self.created_path != created_path {
|
||||
self.created_path = created_path;
|
||||
self.buffers.borrow_mut().clear();
|
||||
@@ -192,6 +193,7 @@ fn render(
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_markup(&text);
|
||||
|
||||
@@ -206,6 +208,7 @@ fn render(
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_markup(&text);
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ fn render(scale: f64) -> anyhow::Result<MemoryBuffer> {
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
@@ -124,6 +125,7 @@ fn render(scale: f64) -> anyhow::Result<MemoryBuffer> {
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(TEXT);
|
||||
|
||||
@@ -87,7 +87,7 @@ impl HotkeyOverlay {
|
||||
let output_size = output_size(output);
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
buffers.retain(|output, _| output.upgrade().is_some());
|
||||
buffers.retain(|output, _| output.is_alive());
|
||||
|
||||
// FIXME: should probably use the working area rather than view size.
|
||||
let weak = output.downgrade();
|
||||
@@ -250,6 +250,7 @@ fn render(
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
|
||||
let bold = AttrList::new();
|
||||
@@ -301,6 +302,7 @@ fn render(
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
@@ -401,6 +403,9 @@ fn key_name(comp_mod: CompositorMod, key: &Key) -> String {
|
||||
if key.modifiers.contains(Modifiers::ISO_LEVEL3_SHIFT) {
|
||||
name.push_str("ISO_Level3_Shift + ");
|
||||
}
|
||||
if key.modifiers.contains(Modifiers::ISO_LEVEL5_SHIFT) {
|
||||
name.push_str("ISO_Level5_Shift + ");
|
||||
}
|
||||
if key.modifiers.contains(Modifiers::SHIFT) {
|
||||
name.push_str("Shift + ");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::time::Duration;
|
||||
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::gles::GlesTexture;
|
||||
use smithay::utils::{Scale, Transform};
|
||||
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
@@ -43,6 +44,14 @@ impl ScreenTransition {
|
||||
self.alpha == 0.
|
||||
}
|
||||
|
||||
pub fn update_render_elements(&mut self, scale: Scale<f64>, transform: Transform) {
|
||||
// These textures should remain full-screen, even if scale or transform changes.
|
||||
for buffer in &mut self.from_texture {
|
||||
buffer.set_texture_scale(scale);
|
||||
buffer.set_texture_transform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, target: RenderTarget) -> PrimaryGpuTextureRenderElement {
|
||||
let idx = match target {
|
||||
RenderTarget::Output => 0,
|
||||
|
||||
+305
-55
@@ -1,42 +1,63 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use arrayvec::ArrayVec;
|
||||
use niri_config::Action;
|
||||
use niri_config::{Action, Config};
|
||||
use pango::{Alignment, FontDescription};
|
||||
use pangocairo::cairo::{self, ImageSurface};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::input::{ButtonState, MouseButton};
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::Kind;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::ExportMem;
|
||||
use smithay::backend::renderer::{ExportMem, Texture as _};
|
||||
use smithay::input::keyboard::{Keysym, ModifiersState};
|
||||
use smithay::output::{Output, WeakOutput};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Size, Transform};
|
||||
use smithay::utils::{Physical, Point, Rectangle, Scale, Size, Transform};
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
|
||||
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
|
||||
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::render_helpers::{render_to_texture, RenderTarget};
|
||||
use crate::utils::to_physical_precise_round;
|
||||
|
||||
const BORDER: i32 = 2;
|
||||
const SELECTION_BORDER: i32 = 2;
|
||||
|
||||
const PADDING: i32 = 8;
|
||||
const FONT: &str = "sans 14px";
|
||||
const BORDER: i32 = 4;
|
||||
const TEXT_HIDE_P: &str =
|
||||
"Press <span face='mono' bgcolor='#2C2C2C'> Space </span> to save the screenshot.\n\
|
||||
Press <span face='mono' bgcolor='#2C2C2C'> P </span> to hide the pointer.";
|
||||
const TEXT_SHOW_P: &str =
|
||||
"Press <span face='mono' bgcolor='#2C2C2C'> Space </span> to save the screenshot.\n\
|
||||
Press <span face='mono' bgcolor='#2C2C2C'> P </span> to show the pointer.";
|
||||
|
||||
// Ideally the screenshot UI should support cross-output selections. However, that poses some
|
||||
// technical challenges when the outputs have different scales and such. So, this implementation
|
||||
// allows only single-output selections for now.
|
||||
//
|
||||
// As a consequence of this, selection coordinates are in output-local coordinate space.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ScreenshotUi {
|
||||
Closed {
|
||||
last_selection: Option<(WeakOutput, Rectangle<i32, Physical>)>,
|
||||
config: Rc<RefCell<Config>>,
|
||||
},
|
||||
Open {
|
||||
selection: (Output, Point<i32, Physical>, Point<i32, Physical>),
|
||||
output_data: HashMap<Output, OutputData>,
|
||||
mouse_down: bool,
|
||||
show_pointer: bool,
|
||||
open_anim: Animation,
|
||||
config: Rc<RefCell<Config>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -45,10 +66,16 @@ pub struct OutputData {
|
||||
scale: f64,
|
||||
transform: Transform,
|
||||
// Output, screencast, screen capture.
|
||||
texture: [GlesTexture; 3],
|
||||
texture_buffer: [TextureBuffer<GlesTexture>; 3],
|
||||
screenshot: [OutputScreenshot; 3],
|
||||
buffers: [SolidColorBuffer; 8],
|
||||
locations: [Point<i32, Physical>; 8],
|
||||
panel: Option<(TextureBuffer<GlesTexture>, TextureBuffer<GlesTexture>)>,
|
||||
}
|
||||
|
||||
pub struct OutputScreenshot {
|
||||
texture: GlesTexture,
|
||||
buffer: PrimaryGpuTextureRenderElement,
|
||||
pointer: Option<PrimaryGpuTextureRenderElement>,
|
||||
}
|
||||
|
||||
niri_render_elements! {
|
||||
@@ -59,24 +86,29 @@ niri_render_elements! {
|
||||
}
|
||||
|
||||
impl ScreenshotUi {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(config: Rc<RefCell<Config>>) -> Self {
|
||||
Self::Closed {
|
||||
last_selection: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
&mut self,
|
||||
renderer: &GlesRenderer,
|
||||
renderer: &mut GlesRenderer,
|
||||
// Output, screencast, screen capture.
|
||||
screenshots: HashMap<Output, [GlesTexture; 3]>,
|
||||
screenshots: HashMap<Output, [OutputScreenshot; 3]>,
|
||||
default_output: Output,
|
||||
) -> bool {
|
||||
if screenshots.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Self::Closed { last_selection } = self else {
|
||||
let Self::Closed {
|
||||
last_selection,
|
||||
config,
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -100,29 +132,19 @@ impl ScreenshotUi {
|
||||
}
|
||||
};
|
||||
|
||||
let scale = selection.0.current_scale().integer_scale();
|
||||
let selection = (
|
||||
selection.0,
|
||||
selection.1.loc,
|
||||
selection.1.loc + selection.1.size - Size::from((scale, scale)),
|
||||
selection.1.loc + selection.1.size - Size::from((1, 1)),
|
||||
);
|
||||
|
||||
let output_data = screenshots
|
||||
.into_iter()
|
||||
.map(|(output, texture)| {
|
||||
.map(|(output, screenshot)| {
|
||||
let transform = output.current_transform();
|
||||
let output_mode = output.current_mode().unwrap();
|
||||
let size = transform.transform_size(output_mode.size);
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let texture_buffer = texture.clone().map(|texture| {
|
||||
TextureBuffer::from_texture(
|
||||
renderer,
|
||||
texture,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
)
|
||||
});
|
||||
let buffers = [
|
||||
SolidColorBuffer::new((0., 0.), [1., 1., 1., 1.]),
|
||||
SolidColorBuffer::new((0., 0.), [1., 1., 1., 1.]),
|
||||
@@ -134,23 +156,41 @@ impl ScreenshotUi {
|
||||
SolidColorBuffer::new((0., 0.), [0., 0., 0., 0.5]),
|
||||
];
|
||||
let locations = [Default::default(); 8];
|
||||
|
||||
let mut render_panel_ = |text| {
|
||||
render_panel(renderer, scale, text)
|
||||
.map_err(|err| warn!("error rendering help panel: {err:?}"))
|
||||
.ok()
|
||||
};
|
||||
let panel_show = render_panel_(TEXT_SHOW_P);
|
||||
let panel_hide = render_panel_(TEXT_HIDE_P);
|
||||
let panel = Option::zip(panel_show, panel_hide);
|
||||
|
||||
let data = OutputData {
|
||||
size,
|
||||
scale,
|
||||
transform,
|
||||
texture,
|
||||
texture_buffer,
|
||||
screenshot,
|
||||
buffers,
|
||||
locations,
|
||||
panel,
|
||||
};
|
||||
(output, data)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let open_anim = {
|
||||
let c = config.borrow();
|
||||
Animation::new(0., 1., 0., c.animations.screenshot_ui_open.0)
|
||||
};
|
||||
|
||||
*self = Self::Open {
|
||||
selection,
|
||||
output_data,
|
||||
mouse_down: false,
|
||||
show_pointer: true,
|
||||
open_anim,
|
||||
config: config.clone(),
|
||||
};
|
||||
|
||||
self.update_buffers();
|
||||
@@ -159,13 +199,11 @@ impl ScreenshotUi {
|
||||
}
|
||||
|
||||
pub fn close(&mut self) -> bool {
|
||||
let selection = match mem::take(self) {
|
||||
Self::Open { selection, .. } => selection,
|
||||
closed @ Self::Closed { .. } => {
|
||||
// Put it back.
|
||||
*self = closed;
|
||||
return false;
|
||||
}
|
||||
let Self::Open {
|
||||
selection, config, ..
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let last_selection = Some((
|
||||
@@ -173,15 +211,40 @@ impl ScreenshotUi {
|
||||
rect_from_corner_points(selection.1, selection.2),
|
||||
));
|
||||
|
||||
*self = Self::Closed { last_selection };
|
||||
*self = Self::Closed {
|
||||
last_selection,
|
||||
config: config.clone(),
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn toggle_pointer(&mut self) {
|
||||
if let Self::Open { show_pointer, .. } = self {
|
||||
*show_pointer = !*show_pointer;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self, ScreenshotUi::Open { .. })
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, current_time: Duration) {
|
||||
let Self::Open { open_anim, .. } = self else {
|
||||
return;
|
||||
};
|
||||
|
||||
open_anim.set_current_time(current_time);
|
||||
}
|
||||
|
||||
pub fn are_animations_ongoing(&self) -> bool {
|
||||
let Self::Open { open_anim, .. } = self else {
|
||||
return false;
|
||||
};
|
||||
|
||||
!open_anim.is_done()
|
||||
}
|
||||
|
||||
fn update_buffers(&mut self) {
|
||||
let Self::Open {
|
||||
selection,
|
||||
@@ -212,7 +275,7 @@ impl ScreenshotUi {
|
||||
*b = rect.loc + rect.size - Size::from((1, 1));
|
||||
}
|
||||
|
||||
let border = to_physical_precise_round(scale, BORDER);
|
||||
let border = to_physical_precise_round(scale, SELECTION_BORDER);
|
||||
|
||||
let resize = move |buffer: &mut SolidColorBuffer, w: i32, h: i32| {
|
||||
let size = Size::<_, Physical>::from((w, h));
|
||||
@@ -259,10 +322,17 @@ impl ScreenshotUi {
|
||||
&self,
|
||||
output: &Output,
|
||||
target: RenderTarget,
|
||||
) -> ArrayVec<ScreenshotUiRenderElement, 9> {
|
||||
) -> ArrayVec<ScreenshotUiRenderElement, 11> {
|
||||
let _span = tracy_client::span!("ScreenshotUi::render_output");
|
||||
|
||||
let Self::Open { output_data, .. } = self else {
|
||||
let Self::Open {
|
||||
output_data,
|
||||
show_pointer,
|
||||
mouse_down,
|
||||
open_anim,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
panic!("screenshot UI must be open to render it");
|
||||
};
|
||||
|
||||
@@ -272,12 +342,40 @@ impl ScreenshotUi {
|
||||
return elements;
|
||||
};
|
||||
|
||||
let scale = output_data.scale;
|
||||
let progress = open_anim.clamped_value().clamp(0., 1.) as f32;
|
||||
|
||||
// The help panel goes on top.
|
||||
if let Some((show, hide)) = &output_data.panel {
|
||||
let buffer = if *show_pointer { hide } else { show };
|
||||
|
||||
let size = buffer.texture().size();
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
let x = max(0, (output_data.size.w - size.w) / 2);
|
||||
let y = max(0, output_data.size.h - size.h - padding * 2);
|
||||
let location = Point::<_, Physical>::from((x, y))
|
||||
.to_f64()
|
||||
.to_logical(scale);
|
||||
|
||||
let alpha = if *mouse_down { 0.3 } else { 0.9 };
|
||||
|
||||
let elem = PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||
buffer.clone(),
|
||||
location,
|
||||
alpha * progress,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
));
|
||||
elements.push(elem.into());
|
||||
}
|
||||
|
||||
let buf_loc = zip(&output_data.buffers, &output_data.locations);
|
||||
elements.extend(buf_loc.map(|(buffer, loc)| {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
buffer,
|
||||
loc.to_f64().to_logical(output_data.scale),
|
||||
1.,
|
||||
loc.to_f64().to_logical(scale),
|
||||
progress,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into()
|
||||
@@ -289,17 +387,14 @@ impl ScreenshotUi {
|
||||
RenderTarget::Screencast => 1,
|
||||
RenderTarget::ScreenCapture => 2,
|
||||
};
|
||||
elements.push(
|
||||
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||
output_data.texture_buffer[index].clone(),
|
||||
(0., 0.),
|
||||
1.,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
let screenshot = &output_data.screenshot[index];
|
||||
|
||||
if *show_pointer {
|
||||
if let Some(pointer) = screenshot.pointer.clone() {
|
||||
elements.push(pointer.into());
|
||||
}
|
||||
}
|
||||
elements.push(screenshot.buffer.clone().into());
|
||||
|
||||
elements
|
||||
}
|
||||
@@ -313,6 +408,7 @@ impl ScreenshotUi {
|
||||
let Self::Open {
|
||||
selection,
|
||||
output_data,
|
||||
show_pointer,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
@@ -321,12 +417,50 @@ impl ScreenshotUi {
|
||||
|
||||
let data = &output_data[&selection.0];
|
||||
let rect = rect_from_corner_points(selection.1, selection.2);
|
||||
|
||||
let screenshot = &data.screenshot[0];
|
||||
|
||||
// Composite the pointer on top if needed.
|
||||
let mut tex_rect = None;
|
||||
if *show_pointer {
|
||||
if let Some(pointer) = screenshot.pointer.clone() {
|
||||
let scale = pointer.0.buffer().texture_scale();
|
||||
let offset = rect.loc.upscale(-1);
|
||||
|
||||
let mut elements = ArrayVec::<_, 2>::new();
|
||||
elements.push(pointer);
|
||||
elements.push(screenshot.buffer.clone());
|
||||
let elements = elements.iter().rev().map(|elem| {
|
||||
RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
|
||||
});
|
||||
|
||||
let res = render_to_texture(
|
||||
renderer,
|
||||
rect.size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Abgr8888,
|
||||
elements,
|
||||
);
|
||||
match res {
|
||||
Ok((texture, _)) => {
|
||||
tex_rect = Some((texture, Rectangle::from_loc_and_size((0, 0), rect.size)));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("error compositing pointer onto screenshot: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (texture, rect) = tex_rect.unwrap_or_else(|| (screenshot.texture.clone(), rect));
|
||||
// The size doesn't actually matter because we're not transforming anything.
|
||||
let buf_rect = rect
|
||||
.to_logical(1)
|
||||
.to_buffer(1, Transform::Normal, &data.size.to_logical(1));
|
||||
.to_buffer(1, Transform::Normal, &Size::from((1, 1)));
|
||||
|
||||
let mapping = renderer
|
||||
.copy_texture(&data.texture[0], buf_rect, Fourcc::Abgr8888)
|
||||
.copy_texture(&texture, buf_rect, Fourcc::Abgr8888)
|
||||
.context("error copying texture")?;
|
||||
let copy = renderer
|
||||
.map_texture(&mapping)
|
||||
@@ -390,6 +524,7 @@ impl ScreenshotUi {
|
||||
selection,
|
||||
output_data,
|
||||
mouse_down,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
return false;
|
||||
@@ -433,9 +568,50 @@ impl ScreenshotUi {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScreenshotUi {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
impl OutputScreenshot {
|
||||
pub fn from_textures(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: Scale<f64>,
|
||||
texture: GlesTexture,
|
||||
pointer: Option<(GlesTexture, Rectangle<i32, Physical>)>,
|
||||
) -> Self {
|
||||
let buffer = PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||
TextureBuffer::from_texture(
|
||||
renderer,
|
||||
texture.clone(),
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
),
|
||||
(0., 0.),
|
||||
1.,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
));
|
||||
|
||||
let pointer = pointer.map(|(texture, geo)| {
|
||||
PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer(
|
||||
TextureBuffer::from_texture(
|
||||
renderer,
|
||||
texture,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
),
|
||||
geo.to_f64().to_logical(scale).loc,
|
||||
1.,
|
||||
None,
|
||||
None,
|
||||
Kind::Unspecified,
|
||||
))
|
||||
});
|
||||
|
||||
Self {
|
||||
texture,
|
||||
buffer,
|
||||
pointer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +630,10 @@ fn action(raw: Keysym, mods: ModifiersState) -> Option<Action> {
|
||||
return Some(Action::ConfirmScreenshot);
|
||||
}
|
||||
|
||||
if !mods.ctrl && raw == Keysym::p {
|
||||
return Some(Action::ScreenshotTogglePointer);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -469,3 +649,73 @@ pub fn rect_from_corner_points(
|
||||
// screen worth of selection we must add back that + 1.
|
||||
Rectangle::from_extemities((x1, y1), (x2 + 1, y2 + 1))
|
||||
}
|
||||
|
||||
fn render_panel(
|
||||
renderer: &mut GlesRenderer,
|
||||
scale: f64,
|
||||
text: &str,
|
||||
) -> anyhow::Result<TextureBuffer<GlesTexture>> {
|
||||
let _span = tracy_client::span!("screenshot_ui::render_panel");
|
||||
|
||||
let padding: i32 = to_physical_precise_round(scale, PADDING);
|
||||
|
||||
// Add 2 px of spacing to separate the backgrounds of the "Space" and "P" keys.
|
||||
let spacing = to_physical_precise_round::<i32>(scale, 2) * 1024;
|
||||
|
||||
let mut font = FontDescription::from_string(FONT);
|
||||
font.set_absolute_size(to_physical_precise_round(scale, font.size()));
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(text);
|
||||
layout.set_spacing(spacing);
|
||||
|
||||
let (mut width, mut height) = layout.pixel_size();
|
||||
width += padding * 2;
|
||||
height += padding * 2;
|
||||
|
||||
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
|
||||
let cr = cairo::Context::new(&surface)?;
|
||||
cr.set_source_rgb(0.1, 0.1, 0.1);
|
||||
cr.paint()?;
|
||||
|
||||
cr.move_to(padding.into(), padding.into());
|
||||
let layout = pangocairo::functions::create_layout(&cr);
|
||||
layout.context().set_round_glyph_positions(false);
|
||||
layout.set_font_description(Some(&font));
|
||||
layout.set_alignment(Alignment::Center);
|
||||
layout.set_markup(text);
|
||||
layout.set_spacing(spacing);
|
||||
|
||||
cr.set_source_rgb(1., 1., 1.);
|
||||
pangocairo::functions::show_layout(&cr, &layout);
|
||||
|
||||
cr.move_to(0., 0.);
|
||||
cr.line_to(width.into(), 0.);
|
||||
cr.line_to(width.into(), height.into());
|
||||
cr.line_to(0., height.into());
|
||||
cr.line_to(0., 0.);
|
||||
cr.set_source_rgb(0.3, 0.3, 0.3);
|
||||
// Keep the border width even to avoid blurry edges.
|
||||
cr.set_line_width((f64::from(BORDER) / 2. * scale).round() * 2.);
|
||||
cr.stroke()?;
|
||||
drop(cr);
|
||||
|
||||
let data = surface.take_data().unwrap();
|
||||
let buffer = TextureBuffer::from_memory(
|
||||
renderer,
|
||||
&data,
|
||||
Fourcc::Argb8888,
|
||||
(width, height),
|
||||
false,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
Vec::new(),
|
||||
)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
+5
-8
@@ -1,11 +1,8 @@
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::atomic::{AtomicU64, 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,
|
||||
value: AtomicU64,
|
||||
}
|
||||
|
||||
impl IdCounter {
|
||||
@@ -13,12 +10,12 @@ impl IdCounter {
|
||||
Self {
|
||||
// Start from 1 to reduce the possibility that some other code that uses these IDs will
|
||||
// get confused.
|
||||
value: AtomicU32::new(1),
|
||||
value: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> u32 {
|
||||
self.value.fetch_add(1, Ordering::SeqCst)
|
||||
pub fn next(&self) -> u64 {
|
||||
self.value.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user